first commit
This commit is contained in:
15
.gitignore
vendored
Normal file
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
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Genie
|
||||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="com.amz.genie" />
|
||||||
|
<option name="mobileSdkAppId" value="1:791153239007:android:a35521285c9d4102f05a6a" />
|
||||||
|
<option name="projectId" value="genie-11548" />
|
||||||
|
<option name="projectNumber" value="791153239007" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/compiler.xml
generated
Normal file
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
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="2026-02-26T07:38:16.877861657Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8N801RLQZ" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/dictionaries/project.xml
generated
Normal file
12
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>aksi</w>
|
||||||
|
<w>komunikasi</w>
|
||||||
|
<w>reaksi</w>
|
||||||
|
<w>tentang</w>
|
||||||
|
<w>tiet</w>
|
||||||
|
<w>tipe</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
18
.idea/gradle.xml
generated
Normal file
18
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/migrations.xml
generated
Normal file
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
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
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>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
151
app/build.gradle.kts
Normal file
151
app/build.gradle.kts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import com.android.build.api.artifact.SingleArtifact
|
||||||
|
import com.android.build.api.variant.BuiltArtifactsLoader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.google.gms.google.services)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readStringResource(projectDir: File, resName: String): String {
|
||||||
|
val xml = File(projectDir, "src/main/res/values/strings.xml")
|
||||||
|
if (!xml.exists()) return "app"
|
||||||
|
|
||||||
|
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xml)
|
||||||
|
val nodes = doc.getElementsByTagName("string")
|
||||||
|
for (i in 0 until nodes.length) {
|
||||||
|
val n = nodes.item(i)
|
||||||
|
val nameAttr = n.attributes?.getNamedItem("name")?.nodeValue
|
||||||
|
if (nameAttr == resName) {
|
||||||
|
return n.textContent.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "app"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.amz.genie"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.amz.genie"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 57
|
||||||
|
versionName = "0.5.7"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task: copy APK artifact -> output folder, rename with timestamp
|
||||||
|
*/
|
||||||
|
abstract class CopyRenameApkTask : DefaultTask() {
|
||||||
|
|
||||||
|
@get:InputDirectory
|
||||||
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
abstract val inputApkFolder: DirectoryProperty
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@get:Internal
|
||||||
|
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
abstract val appName: Property<String>
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun run() {
|
||||||
|
val outDirFile = outputDir.get().asFile
|
||||||
|
outDirFile.deleteRecursively()
|
||||||
|
outDirFile.mkdirs()
|
||||||
|
|
||||||
|
val builtArtifacts = builtArtifactsLoader.get().load(inputApkFolder.get())
|
||||||
|
?: error("Cannot load APK artifacts from ${inputApkFolder.get().asFile}")
|
||||||
|
|
||||||
|
val ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"))
|
||||||
|
val prefix = appName.get()
|
||||||
|
|
||||||
|
builtArtifacts.elements.forEach { artifact ->
|
||||||
|
val src = File(artifact.outputFile)
|
||||||
|
|
||||||
|
// Kalau ada splits, bisa lebih dari 1 apk. Kita bedakan pakai versionCode/variantName optional.
|
||||||
|
val name = buildString {
|
||||||
|
append(prefix).append("-").append(ts)
|
||||||
|
artifact.versionCode?.let { append("-").append(it) }
|
||||||
|
append(".apk")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dst = File(outDirFile, name)
|
||||||
|
Files.copy(src.toPath(), dst.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants(selector().withBuildType("debug")) { variant ->
|
||||||
|
val t = tasks.register("copyRename${variant.name.replaceFirstChar { it.uppercase() }}Apk", CopyRenameApkTask::class.java) {
|
||||||
|
val label = readStringResource(project.projectDir, "app_name")
|
||||||
|
appName.set(label)
|
||||||
|
outputDir.set(layout.buildDirectory.dir(variant.name))
|
||||||
|
builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen ke artifact APK: task otomatis jalan saat APK dibuat (assembleDebug, dll)
|
||||||
|
variant.artifacts
|
||||||
|
.use(t)
|
||||||
|
.wiredWith { it.inputApkFolder }
|
||||||
|
.toListenTo(SingleArtifact.APK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.material)
|
||||||
|
implementation(libs.androidx.activity)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
|
implementation(libs.lottie)
|
||||||
|
implementation(libs.sdp.android)
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.converter.gson)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
implementation(libs.glide)
|
||||||
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
|
|
||||||
|
implementation(platform(libs.firebase.bom))
|
||||||
|
implementation(libs.firebase.analytics)
|
||||||
|
implementation(libs.firebase.messaging)
|
||||||
|
implementation(libs.firebase.auth)
|
||||||
|
|
||||||
|
implementation(libs.androidx.emoji2)
|
||||||
|
implementation(libs.androidx.emoji2.views.helper)
|
||||||
|
implementation(libs.androidx.emoji2.emojipicker)
|
||||||
|
|
||||||
|
}
|
||||||
BIN
app/debug/app-debug.apk
Normal file
BIN
app/debug/app-debug.apk
Normal file
Binary file not shown.
21
app/debug/output-metadata.json
Normal file
21
app/debug/output-metadata.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "com.amz.genie",
|
||||||
|
"variantName": "debug",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 57,
|
||||||
|
"versionName": "0.5.7",
|
||||||
|
"outputFile": "app-debug.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File",
|
||||||
|
"minSdkVersionForDexing": 26
|
||||||
|
}
|
||||||
29
app/google-services.json
Normal file
29
app/google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "791153239007",
|
||||||
|
"project_id": "genie-11548",
|
||||||
|
"storage_bucket": "genie-11548.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:791153239007:android:a35521285c9d4102f05a6a",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.amz.genie"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBN5-kXCfqw_7o5O1dzA-oWiA2SH4wqQAY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
21
app/proguard-rules.pro
vendored
Normal file
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.amz.genie
|
||||||
|
|
||||||
|
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.amz.genie", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
app/src/main/AndroidManifest.xml
Normal file
81
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?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-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".services.APIMain"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@drawable/logo_normal"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@drawable/logo_normal"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.GENIE"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
<activity
|
||||||
|
android:name=".activities.AttachmentPreviewActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.AddTemplateActionActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.MoreActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.AddCustomActionActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.GeneralSubDetailActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.GeneralDetailActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.LoginActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.MainActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SplashActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.MyFirebaseMessagingService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
1
app/src/main/assets/lottie/loader_circle.json
Normal file
1
app/src/main/assets/lottie/loader_circle.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/main/assets/lottie/loader_send.json
Normal file
1
app/src/main/assets/lottie/loader_send.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/main/assets/lottie/loading.json
Normal file
1
app/src/main/assets/lottie/loading.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/main/assets/lottie/loading_alt.json
Normal file
1
app/src/main/assets/lottie/loading_alt.json
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,645 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.AutoCompleteTextView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.adapters.AttachmentAdapter
|
||||||
|
import com.amz.genie.adapters.RecipientAdapter
|
||||||
|
import com.amz.genie.adapters.RecipientPickerAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.helpers.Utils.uriToMultipartPart
|
||||||
|
import com.amz.genie.models.AttachmentItem
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import com.amz.genie.models.Pegawai
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.models.PostAksi
|
||||||
|
import com.amz.genie.models.Tentang
|
||||||
|
import com.amz.genie.models.TipeKomunikasi
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AddCustomActionActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var ibBack: ImageButton
|
||||||
|
private lateinit var actvTipeKomunikasi: AutoCompleteTextView
|
||||||
|
private lateinit var actvTentang: AutoCompleteTextView
|
||||||
|
|
||||||
|
private lateinit var rvRecipient: RecyclerView
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var tietDescription: TextInputEditText
|
||||||
|
private lateinit var btAttachment: MaterialButton
|
||||||
|
private lateinit var btSend: MaterialButton
|
||||||
|
private lateinit var llAttachmentsContainer: View
|
||||||
|
private lateinit var rvAttachments: RecyclerView
|
||||||
|
private lateinit var tvAddRecipient: TextView
|
||||||
|
private lateinit var attachmentAdapter: AttachmentAdapter
|
||||||
|
|
||||||
|
private lateinit var recipientAdapter: RecipientAdapter
|
||||||
|
private val recipientOptions = mutableListOf<Pegawai>()
|
||||||
|
private val selectedRecipients = mutableListOf<Pegawai>()
|
||||||
|
private var idTipeKomunikasi = ""
|
||||||
|
private var idTentang = ""
|
||||||
|
private var cameraOutputUri: Uri? = null
|
||||||
|
private val attachments = mutableListOf<AttachmentItem>()
|
||||||
|
private val takePictureLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||||
|
if (success) {
|
||||||
|
cameraOutputUri?.let { addAttachment(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pickFileLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addAttachment(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestCameraPermissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
openCamera()
|
||||||
|
} else {
|
||||||
|
showSnack("Izin kamera ditolak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCameraWithPermission() {
|
||||||
|
val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
if (granted) {
|
||||||
|
openCamera()
|
||||||
|
} else {
|
||||||
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_add_custom_action)
|
||||||
|
|
||||||
|
initUI(savedInstanceState)
|
||||||
|
renderAttachments()
|
||||||
|
setupDropdowns()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(savedInstanceState: Bundle?) {
|
||||||
|
ibBack = findViewById(R.id.ib_back_add_custom_action)
|
||||||
|
actvTipeKomunikasi = findViewById(R.id.actv_tipe_komunikasi_add_custom_action)
|
||||||
|
actvTentang = findViewById(R.id.actv_tentang_add_custom_action)
|
||||||
|
rvRecipient = findViewById(R.id.rv_recipient_add_custom_action)
|
||||||
|
tvEmpty = findViewById(R.id.tv_empty_add_custom_action)
|
||||||
|
btAttachment = findViewById(R.id.btn_attachment_add_custom_action)
|
||||||
|
btSend = findViewById(R.id.btn_send_add_custom_action)
|
||||||
|
llAttachmentsContainer = findViewById(R.id.ll_attachments_container_add_custom_action)
|
||||||
|
rvAttachments = findViewById(R.id.rv_attachments_add_custom_action)
|
||||||
|
tvAddRecipient = findViewById(R.id.tv_lbl_recipient_add_custom_action)
|
||||||
|
tietDescription = findViewById(R.id.et_description_add_custom_action)
|
||||||
|
|
||||||
|
recipientAdapter = RecipientAdapter { pegawai ->
|
||||||
|
val idx = selectedRecipients.indexOfFirst { it.kode == pegawai.kode }
|
||||||
|
if (idx != -1) {
|
||||||
|
selectedRecipients.removeAt(idx)
|
||||||
|
renderRecipients()
|
||||||
|
showSnack("Penerima dihapus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rvRecipient.layoutManager = LinearLayoutManager(this)
|
||||||
|
rvRecipient.adapter = recipientAdapter
|
||||||
|
|
||||||
|
attachmentAdapter = AttachmentAdapter { item ->
|
||||||
|
attachments.remove(item)
|
||||||
|
renderAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
rvAttachments.layoutManager = GridLayoutManager(this, 2)
|
||||||
|
rvAttachments.adapter = attachmentAdapter
|
||||||
|
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRecipients() {
|
||||||
|
val hasData = selectedRecipients.isNotEmpty()
|
||||||
|
rvRecipient.visibility = if (hasData) View.VISIBLE else View.GONE
|
||||||
|
tvEmpty.visibility = if (hasData) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
recipientAdapter.submitList(selectedRecipients.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDropdowns() {
|
||||||
|
val communicationTypeList = ArrayList<String>()
|
||||||
|
APIMain.require().selectionServices.communicationTypes(
|
||||||
|
Preferences.getAccessToken(this))
|
||||||
|
.enqueue(object: Callback<ArrayList<TipeKomunikasi>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<TipeKomunikasi>>,
|
||||||
|
response: Response<ArrayList<TipeKomunikasi>>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
communicationTypeList.clear()
|
||||||
|
body.forEach { communicationTypeList.add(it.tipe_komunikasi) }
|
||||||
|
|
||||||
|
val adapterTipe = ArrayAdapter(this@AddCustomActionActivity,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
communicationTypeList)
|
||||||
|
actvTipeKomunikasi.setAdapter(adapterTipe)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddCustomActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<ArrayList<TipeKomunikasi>>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
handleBackPress(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ibBack.setOnClickListener { handleBackPress(0) }
|
||||||
|
|
||||||
|
actvTipeKomunikasi.setOnItemClickListener { parent, _, position, _ ->
|
||||||
|
val selected = parent.getItemAtPosition(position).toString()
|
||||||
|
idTipeKomunikasi = when (selected) {
|
||||||
|
"Bertanya" -> "A"
|
||||||
|
"Informasi" -> "I"
|
||||||
|
"Laporan" -> "L"
|
||||||
|
"Approval" -> "P"
|
||||||
|
"Pengajuan" -> "R"
|
||||||
|
"Penugasan" -> "T"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val tentangList = ArrayList<String>()
|
||||||
|
APIMain.require().selectionServices.tentangs(
|
||||||
|
Preferences.getAccessToken(this), idTipeKomunikasi)
|
||||||
|
.enqueue(object: Callback<ArrayList<Tentang>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Tentang>>,
|
||||||
|
response: Response<ArrayList<Tentang>>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
body.forEach { tentangList.add(it.tentang) }
|
||||||
|
|
||||||
|
val adapterTentang = ArrayAdapter(this@AddCustomActionActivity,
|
||||||
|
android.R.layout.simple_list_item_1,tentangList)
|
||||||
|
actvTentang.setAdapter(adapterTentang)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddCustomActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<ArrayList<Tentang>>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
actvTipeKomunikasi.setOnClickListener { actvTipeKomunikasi.showDropDown() }
|
||||||
|
|
||||||
|
actvTentang.setOnClickListener { actvTentang.showDropDown() }
|
||||||
|
|
||||||
|
actvTentang.setOnItemClickListener { parent, _, position, _ ->
|
||||||
|
selectedRecipients.clear()
|
||||||
|
renderRecipients()
|
||||||
|
|
||||||
|
val selectedTentang = parent.getItemAtPosition(position).toString()
|
||||||
|
idTentang = when (selectedTentang) {
|
||||||
|
"Aturan" -> "A"
|
||||||
|
"Barang" -> "B"
|
||||||
|
"Kegiatan" -> "G"
|
||||||
|
"Jadwal" -> "J"
|
||||||
|
"Keadaan" -> "K"
|
||||||
|
"Layanan" -> "L"
|
||||||
|
"Orang" -> "O"
|
||||||
|
"Sistem" -> "S"
|
||||||
|
"Transaksi" -> "T"
|
||||||
|
"Uang" -> "U"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
APIMain.require().selectionServices.recipients(
|
||||||
|
Preferences.getAccessToken(this), idTipeKomunikasi, idTentang)
|
||||||
|
.enqueue(object: Callback<ArrayList<Pegawai>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
response: Response<ArrayList<Pegawai>>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddCustomActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
val filtered = body.filter { p ->
|
||||||
|
p.kode.trim() != userData.pegawai?.kode
|
||||||
|
}
|
||||||
|
|
||||||
|
recipientOptions.clear()
|
||||||
|
recipientOptions.addAll(filtered)
|
||||||
|
|
||||||
|
selectedRecipients.clear()
|
||||||
|
selectedRecipients.addAll(filtered)
|
||||||
|
renderRecipients()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddCustomActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
btAttachment.setOnClickListener {
|
||||||
|
showAttachmentPickerDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
tvAddRecipient.setOnClickListener {
|
||||||
|
if (isNetworkAvailable(this)) {
|
||||||
|
showProgressDialog(true)
|
||||||
|
APIMain.require().selectionServices.allRecipient(
|
||||||
|
Preferences.getAccessToken(this))
|
||||||
|
.enqueue(object: Callback<ArrayList<Pegawai>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
response: Response<ArrayList<Pegawai>>
|
||||||
|
) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddCustomActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
val filtered = body.filter { p ->
|
||||||
|
p.kode.trim() != userData.pegawai?.kode
|
||||||
|
}
|
||||||
|
showRecipientPickerDialog(ArrayList(filtered))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddCustomActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
|
ContextCompat.getString(this@AddCustomActionActivity,
|
||||||
|
R.string.no_internet_message),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btSend.setOnClickListener {
|
||||||
|
showProgressDialog(true)
|
||||||
|
val semuaKodePegawai: List<String> = selectedRecipients.map { it.kode }
|
||||||
|
|
||||||
|
val newAksi = PostAksi(
|
||||||
|
null, semuaKodePegawai, idTentang, idTipeKomunikasi,
|
||||||
|
tietDescription.text.toString(), null
|
||||||
|
)
|
||||||
|
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.serializeNulls()
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val jsonData = gson.toJson(newAksi)
|
||||||
|
val dataBody: RequestBody =
|
||||||
|
jsonData.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||||
|
|
||||||
|
val filesParts: List<MultipartBody.Part> =
|
||||||
|
attachments.map { a ->
|
||||||
|
this.uriToMultipartPart(
|
||||||
|
partName = "files",
|
||||||
|
uri = a.uri,
|
||||||
|
filenameOverride = a.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val filesPartsOrNull = filesParts.takeIf { it.isNotEmpty() }
|
||||||
|
APIMain.require().actionServices.add(
|
||||||
|
token = Preferences.getAccessToken(this@AddCustomActionActivity),
|
||||||
|
data = dataBody,
|
||||||
|
files = filesPartsOrNull
|
||||||
|
).enqueue(object : Callback<Message> {
|
||||||
|
override fun onResponse(call: Call<Message>, response: Response<Message>) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
showSnack("Berhasil kirim aksi")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddCustomActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<Message>, t: Throwable) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
showSnack(t.message ?: "Gagal kirim")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showRecipientPickerDialog(allRecipient: ArrayList<Pegawai>) {
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddCustomActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
// ✅ buang pengirim dulu
|
||||||
|
val cleaned = allRecipient.filter { it.kode.trim() != userData.pegawai?.kode }
|
||||||
|
|
||||||
|
if (cleaned.isEmpty()) {
|
||||||
|
showSnack("Daftar penerima kosong")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base list: hanya yang belum ada di selectedRecipients
|
||||||
|
val baseList = cleaned.filter { ar ->
|
||||||
|
selectedRecipients.none { it.kode == ar.kode }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseList.isEmpty()) {
|
||||||
|
showSnack("Semua penerima sudah dipilih")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val v = layoutInflater.inflate(R.layout.dialog_recipient_picker, null)
|
||||||
|
val rv = v.findViewById<RecyclerView>(R.id.rv_recipient_picker)
|
||||||
|
val sv = v.findViewById<androidx.appcompat.widget.SearchView>(R.id.sv_recipient)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle("Pilih Penerima")
|
||||||
|
.setView(v)
|
||||||
|
.setNegativeButton("Tutup", null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val pickerAdapter = RecipientPickerAdapter { picked ->
|
||||||
|
val exists = selectedRecipients.any { it.kode == picked.kode }
|
||||||
|
if (exists) {
|
||||||
|
showSnack("Penerima sudah dipilih")
|
||||||
|
return@RecipientPickerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRecipients.add(picked)
|
||||||
|
renderRecipients()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.layoutManager = LinearLayoutManager(this)
|
||||||
|
rv.adapter = pickerAdapter
|
||||||
|
rv.setHasFixedSize(true)
|
||||||
|
|
||||||
|
// tampil awal
|
||||||
|
pickerAdapter.submitList(baseList)
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
|
fun applyFilter(query: String?) {
|
||||||
|
val q = query.orEmpty().trim()
|
||||||
|
if (q.isEmpty()) {
|
||||||
|
pickerAdapter.submitList(baseList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val filtered = baseList.filter { p ->
|
||||||
|
val nama = p.nama
|
||||||
|
val kode = p.kode
|
||||||
|
nama.contains(q, ignoreCase = true) || kode.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pickerAdapter.submitList(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
sv.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
applyFilter(query)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
applyFilter(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
sv.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAttachmentPickerDialog() {
|
||||||
|
val items = arrayOf("Kamera", "File Manager")
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle("Pilih Attachment")
|
||||||
|
.setItems(items) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> openCameraWithPermission()
|
||||||
|
1 -> openFileManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("Batal", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCamera() {
|
||||||
|
val imageFile = File.createTempFile(
|
||||||
|
"ATTACH_",
|
||||||
|
".jpg",
|
||||||
|
cacheDir
|
||||||
|
)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
"${packageName}.fileprovider",
|
||||||
|
imageFile
|
||||||
|
)
|
||||||
|
|
||||||
|
cameraOutputUri = uri
|
||||||
|
takePictureLauncher.launch(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openFileManager() {
|
||||||
|
pickFileLauncher.launch(arrayOf("image/*", "application/pdf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addAttachment(uri: Uri) {
|
||||||
|
if (attachments.any { it.uri == uri }) {
|
||||||
|
showSnack("Lampiran sudah ditambahkan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val mime = contentResolver.getType(uri).orEmpty()
|
||||||
|
val name = queryDisplayName(uri) ?: "Attachment"
|
||||||
|
attachments.add(AttachmentItem(uri, name, mime))
|
||||||
|
renderAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAttachments() {
|
||||||
|
val hasData = attachments.isNotEmpty()
|
||||||
|
llAttachmentsContainer.visibility = if (hasData) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
attachmentAdapter.submitList(attachments.toList())
|
||||||
|
rvAttachments.visibility = if (hasData) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
val count = attachments.size
|
||||||
|
btAttachment.text = if (count > 0) "Lampiran ($count)" else getString(R.string.lbl_lampiran)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryDisplayName(uri: Uri): String? {
|
||||||
|
val projection = arrayOf(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||||
|
return runCatching {
|
||||||
|
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||||
|
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (nameIndex == -1) return@use null
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getString(nameIndex)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,989 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.text.InputType
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.adapters.RecipientAdapter
|
||||||
|
import com.amz.genie.adapters.RecipientPickerAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.SimpleTextWatcher
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.models.AddActionItem
|
||||||
|
import com.amz.genie.models.FormAttachment
|
||||||
|
import com.amz.genie.models.KomunikasiDetail
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import com.amz.genie.models.Pegawai
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.collections.emptyList
|
||||||
|
|
||||||
|
class AddTemplateActionActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var ibBack: ImageButton
|
||||||
|
private lateinit var tvKomunikasi: TextView
|
||||||
|
private lateinit var rvRecipient: RecyclerView
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var tvAddRecipient: TextView
|
||||||
|
private lateinit var recipientAdapter: RecipientAdapter
|
||||||
|
private lateinit var actionItem: AddActionItem
|
||||||
|
private lateinit var btSend: Button
|
||||||
|
|
||||||
|
private val recipientOptions = mutableListOf<Pegawai>()
|
||||||
|
private val selectedRecipients = mutableListOf<Pegawai>()
|
||||||
|
|
||||||
|
// value form dinamis: Int / Double / String / MutableList<String> / MutableList<FormAttachment>
|
||||||
|
private val formValues = linkedMapOf<Int, Any?>()
|
||||||
|
|
||||||
|
// untuk validasi & setError field angka/pecahan/text/tanggal/waktu/jam/string
|
||||||
|
private val fieldTilByKode = linkedMapOf<Int, TextInputLayout>()
|
||||||
|
private val fieldEtByKode = linkedMapOf<Int, TextInputEditText>()
|
||||||
|
|
||||||
|
// untuk list (error wajib list)
|
||||||
|
private val listInputEtByKode = linkedMapOf<Int, TextInputEditText>()
|
||||||
|
|
||||||
|
// untuk lampiran (tampilan list nama file + error text)
|
||||||
|
private val lampiranTvFilesByKode = linkedMapOf<Int, TextView>()
|
||||||
|
private val lampiranTvErrorByKode = linkedMapOf<Int, TextView>()
|
||||||
|
|
||||||
|
private val sdfDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
private val sdfDateTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
private val sdfTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
|
|
||||||
|
private var currentLampiranKode: Int? = null
|
||||||
|
|
||||||
|
private val pickLampiranLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
|
||||||
|
val kode = currentLampiranKode ?: return@registerForActivityResult
|
||||||
|
currentLampiranKode = null
|
||||||
|
|
||||||
|
val list = (formValues[kode] as? MutableList<FormAttachment>) ?: mutableListOf()
|
||||||
|
|
||||||
|
for (u in uris) {
|
||||||
|
val meta = readUriMeta(u) ?: continue
|
||||||
|
if (list.any { it.uri == u }) continue // avoid duplicate
|
||||||
|
list.add(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
formValues[kode] = list
|
||||||
|
|
||||||
|
// refresh UI
|
||||||
|
refreshLampiranUI(kode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_add_template_action)
|
||||||
|
initUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI() {
|
||||||
|
ibBack = findViewById(R.id.ib_back_add_template_action)
|
||||||
|
tvKomunikasi = findViewById(R.id.tv_komunikasi_add_template_action)
|
||||||
|
rvRecipient = findViewById(R.id.rv_recipient_add_template_action)
|
||||||
|
tvEmpty = findViewById(R.id.tv_empty_add_template_action)
|
||||||
|
tvAddRecipient = findViewById(R.id.tv_lbl_recipient_add_template_action)
|
||||||
|
btSend = findViewById(R.id.bt_send_add_template_action)
|
||||||
|
|
||||||
|
val intentDataJson = intent.getStringExtra("data") ?: return
|
||||||
|
actionItem = Gson().fromJson(intentDataJson, AddActionItem::class.java)
|
||||||
|
|
||||||
|
tvKomunikasi.text = actionItem.title.orEmpty()
|
||||||
|
|
||||||
|
recipientAdapter = RecipientAdapter { pegawai ->
|
||||||
|
val idx = selectedRecipients.indexOfFirst { it.kode == pegawai.kode }
|
||||||
|
if (idx != -1) {
|
||||||
|
selectedRecipients.removeAt(idx)
|
||||||
|
renderRecipients()
|
||||||
|
showSnack("Penerima dihapus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rvRecipient.layoutManager = LinearLayoutManager(this)
|
||||||
|
rvRecipient.adapter = recipientAdapter
|
||||||
|
|
||||||
|
val details = actionItem.komunikasi_detail.orEmpty()
|
||||||
|
if (details.isNotEmpty()) {
|
||||||
|
renderDynamicForm(details)
|
||||||
|
} else {
|
||||||
|
findViewById<LinearLayout>(R.id.ll_add_template_action).removeAllViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
initData()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Dynamic Form Renderer
|
||||||
|
// =========================
|
||||||
|
private fun renderDynamicForm(details: List<KomunikasiDetail>) {
|
||||||
|
val container = findViewById<LinearLayout>(R.id.ll_add_template_action)
|
||||||
|
|
||||||
|
container.removeAllViews()
|
||||||
|
formValues.clear()
|
||||||
|
fieldTilByKode.clear()
|
||||||
|
fieldEtByKode.clear()
|
||||||
|
listInputEtByKode.clear()
|
||||||
|
lampiranTvFilesByKode.clear()
|
||||||
|
lampiranTvErrorByKode.clear()
|
||||||
|
|
||||||
|
val fields = details
|
||||||
|
.filter { it.is_aktif == 1 }
|
||||||
|
.sortedBy { it.urutan ?: Int.MAX_VALUE }
|
||||||
|
|
||||||
|
val inflater = LayoutInflater.from(this)
|
||||||
|
|
||||||
|
for (d in fields) {
|
||||||
|
val kode = d.kode ?: continue
|
||||||
|
val label = d.isian?.trim().orEmpty().ifBlank { "Field" }
|
||||||
|
val wajib = d.is_wajib == 1
|
||||||
|
val jenisId = d.id_jenis_isian ?: 6 // fallback string
|
||||||
|
|
||||||
|
when (jenisId) {
|
||||||
|
|
||||||
|
// 4 = angka
|
||||||
|
4 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
et.inputType = InputType.TYPE_CLASS_NUMBER
|
||||||
|
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
et.setText("0")
|
||||||
|
formValues[kode] = 0
|
||||||
|
|
||||||
|
et.addTextChangedListener(SimpleTextWatcher { text ->
|
||||||
|
til.error = null
|
||||||
|
val value = text.trim()
|
||||||
|
formValues[kode] = value.toIntOrNull() ?: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 = pecahan
|
||||||
|
5 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||||
|
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
et.setText("0.00")
|
||||||
|
formValues[kode] = 0.00
|
||||||
|
|
||||||
|
et.addTextChangedListener(SimpleTextWatcher { text ->
|
||||||
|
til.error = null
|
||||||
|
val value = text.trim().replace(",", ".")
|
||||||
|
formValues[kode] = value.toDoubleOrNull() ?: 0.00
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 = tanggal (yyyy-MM-dd)
|
||||||
|
2 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
setupDateField(kode, et, til)
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 = waktu (yyyy-MM-dd HH:mm:ss)
|
||||||
|
1 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
setupDateTimeField(kode, et, til)
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 = jam (HH:mm:ss)
|
||||||
|
3 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
setupTimeField(kode, et, til)
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10 = list
|
||||||
|
10 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_list, container, false)
|
||||||
|
val tvLabel = v.findViewById<TextView>(R.id.tv_label)
|
||||||
|
val etItem = v.findViewById<TextInputEditText>(R.id.et_item)
|
||||||
|
val btnAdd = v.findViewById<View>(R.id.btn_add)
|
||||||
|
val tvItems = v.findViewById<TextView>(R.id.tv_items)
|
||||||
|
|
||||||
|
tvLabel.text = if (wajib) "$label *" else label
|
||||||
|
|
||||||
|
val items = mutableListOf<String>()
|
||||||
|
formValues[kode] = items
|
||||||
|
listInputEtByKode[kode] = etItem
|
||||||
|
|
||||||
|
fun refreshItems() {
|
||||||
|
tvItems.text =
|
||||||
|
if (items.isEmpty()) "Belum ada item"
|
||||||
|
else items.joinToString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
btnAdd.setOnClickListener {
|
||||||
|
etItem.error = null
|
||||||
|
val t = etItem.text?.toString().orEmpty().trim()
|
||||||
|
if (t.isEmpty()) {
|
||||||
|
etItem.error = "Item tidak boleh kosong"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
if (items.any { it.equals(t, true) }) {
|
||||||
|
showSnack("Item sudah ada")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(t)
|
||||||
|
etItem.setText("")
|
||||||
|
refreshItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshItems()
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11 = lampiran (multi)
|
||||||
|
11 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_attachment, container, false)
|
||||||
|
val tvLabel = v.findViewById<TextView>(R.id.tv_label)
|
||||||
|
val btnPick = v.findViewById<View>(R.id.btn_pick)
|
||||||
|
val btnClear = v.findViewById<View>(R.id.btn_clear)
|
||||||
|
val tvFiles = v.findViewById<TextView>(R.id.tv_files)
|
||||||
|
val tvError = v.findViewById<TextView>(R.id.tv_error)
|
||||||
|
|
||||||
|
tvLabel.text = if (wajib) "$label *" else label
|
||||||
|
|
||||||
|
val list = mutableListOf<FormAttachment>()
|
||||||
|
formValues[kode] = list
|
||||||
|
|
||||||
|
lampiranTvFilesByKode[kode] = tvFiles
|
||||||
|
lampiranTvErrorByKode[kode] = tvError
|
||||||
|
|
||||||
|
btnPick.setOnClickListener {
|
||||||
|
tvError.visibility = View.GONE
|
||||||
|
currentLampiranKode = kode
|
||||||
|
pickLampiranLauncher.launch(arrayOf("*/*"))
|
||||||
|
}
|
||||||
|
|
||||||
|
btnClear.setOnClickListener {
|
||||||
|
list.clear()
|
||||||
|
formValues[kode] = list
|
||||||
|
refreshLampiranUI(kode)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshLampiranUI(kode)
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 = teks (multiline)
|
||||||
|
0 -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
|
||||||
|
et.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
et.minLines = 3
|
||||||
|
et.maxLines = 8
|
||||||
|
et.setSingleLine(false)
|
||||||
|
et.setHorizontallyScrolling(false)
|
||||||
|
et.gravity = Gravity.TOP
|
||||||
|
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
et.addTextChangedListener(SimpleTextWatcher { text ->
|
||||||
|
til.error = null
|
||||||
|
formValues[kode] = text
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6 = string (single line) + fallback default
|
||||||
|
else -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_dynamic_input, container, false)
|
||||||
|
val til = v.findViewById<TextInputLayout>(R.id.til)
|
||||||
|
val et = v.findViewById<TextInputEditText>(R.id.et)
|
||||||
|
|
||||||
|
til.hint = if (wajib) "$label *" else label
|
||||||
|
et.inputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
|
||||||
|
fieldTilByKode[kode] = til
|
||||||
|
fieldEtByKode[kode] = et
|
||||||
|
|
||||||
|
et.addTextChangedListener(SimpleTextWatcher { text ->
|
||||||
|
til.error = null
|
||||||
|
formValues[kode] = text
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshLampiranUI(kode: Int) {
|
||||||
|
val tvFiles = lampiranTvFilesByKode[kode] ?: return
|
||||||
|
val list = (formValues[kode] as? List<FormAttachment>).orEmpty()
|
||||||
|
|
||||||
|
tvFiles.text = if (list.isEmpty()) {
|
||||||
|
"Belum ada file"
|
||||||
|
} else {
|
||||||
|
list.joinToString("\n") { "• ${it.fileName}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDateField(kode: Int, et: TextInputEditText, til: TextInputLayout) {
|
||||||
|
et.isFocusable = false
|
||||||
|
et.isFocusableInTouchMode = false
|
||||||
|
et.isClickable = true
|
||||||
|
|
||||||
|
val today = sdfDate.format(Calendar.getInstance().time)
|
||||||
|
if (et.text.isNullOrBlank()) et.setText(today)
|
||||||
|
formValues[kode] = et.text?.toString().orEmpty()
|
||||||
|
|
||||||
|
fun openPicker() {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
|
val currentText = et.text?.toString().orEmpty().trim()
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
runCatching {
|
||||||
|
sdfDate.isLenient = false
|
||||||
|
cal.time = sdfDate.parse(currentText)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
this,
|
||||||
|
{ _, y, m, d ->
|
||||||
|
val picked = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d)
|
||||||
|
til.error = null
|
||||||
|
et.setText(picked)
|
||||||
|
formValues[kode] = picked
|
||||||
|
},
|
||||||
|
cal.get(Calendar.YEAR),
|
||||||
|
cal.get(Calendar.MONTH),
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH)
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
et.setOnClickListener { openPicker() }
|
||||||
|
et.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) openPicker() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupTimeField(kode: Int, et: TextInputEditText, til: TextInputLayout) {
|
||||||
|
et.isFocusable = false
|
||||||
|
et.isFocusableInTouchMode = false
|
||||||
|
et.isClickable = true
|
||||||
|
|
||||||
|
val now = Calendar.getInstance()
|
||||||
|
val def = String.format(
|
||||||
|
Locale.getDefault(),
|
||||||
|
"%02d:%02d:%02d",
|
||||||
|
now.get(Calendar.HOUR_OF_DAY),
|
||||||
|
now.get(Calendar.MINUTE),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if (et.text.isNullOrBlank()) et.setText(def)
|
||||||
|
formValues[kode] = et.text?.toString().orEmpty()
|
||||||
|
|
||||||
|
fun openPicker() {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
val current = et.text?.toString().orEmpty().trim()
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
val parts = current.split(":")
|
||||||
|
if (parts.size >= 2) {
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, parts[0].toIntOrNull() ?: cal.get(Calendar.HOUR_OF_DAY))
|
||||||
|
cal.set(Calendar.MINUTE, parts[1].toIntOrNull() ?: cal.get(Calendar.MINUTE))
|
||||||
|
cal.set(Calendar.SECOND, parts.getOrNull(2)?.toIntOrNull() ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimePickerDialog(
|
||||||
|
this,
|
||||||
|
{ _, h, m ->
|
||||||
|
val picked = String.format(Locale.getDefault(), "%02d:%02d:%02d", h, m, 0)
|
||||||
|
til.error = null
|
||||||
|
et.setText(picked)
|
||||||
|
formValues[kode] = picked
|
||||||
|
},
|
||||||
|
cal.get(Calendar.HOUR_OF_DAY),
|
||||||
|
cal.get(Calendar.MINUTE),
|
||||||
|
true
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
et.setOnClickListener { openPicker() }
|
||||||
|
et.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) openPicker() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDateTimeField(kode: Int, et: TextInputEditText, til: TextInputLayout) {
|
||||||
|
et.isFocusable = false
|
||||||
|
et.isFocusableInTouchMode = false
|
||||||
|
et.isClickable = true
|
||||||
|
|
||||||
|
val nowStr = sdfDateTime.format(Calendar.getInstance().time)
|
||||||
|
if (et.text.isNullOrBlank()) et.setText(nowStr)
|
||||||
|
formValues[kode] = et.text?.toString().orEmpty()
|
||||||
|
|
||||||
|
fun openDateThenTime() {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
|
val currentText = et.text?.toString().orEmpty().trim()
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
runCatching {
|
||||||
|
sdfDateTime.isLenient = false
|
||||||
|
cal.time = sdfDateTime.parse(currentText)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
this,
|
||||||
|
{ _, y, m, d ->
|
||||||
|
TimePickerDialog(
|
||||||
|
this,
|
||||||
|
{ _, hh, mm ->
|
||||||
|
cal.set(Calendar.YEAR, y)
|
||||||
|
cal.set(Calendar.MONTH, m)
|
||||||
|
cal.set(Calendar.DAY_OF_MONTH, d)
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, hh)
|
||||||
|
cal.set(Calendar.MINUTE, mm)
|
||||||
|
cal.set(Calendar.SECOND, 0)
|
||||||
|
|
||||||
|
val picked = sdfDateTime.format(cal.time)
|
||||||
|
til.error = null
|
||||||
|
et.setText(picked)
|
||||||
|
formValues[kode] = picked
|
||||||
|
},
|
||||||
|
cal.get(Calendar.HOUR_OF_DAY),
|
||||||
|
cal.get(Calendar.MINUTE),
|
||||||
|
true
|
||||||
|
).show()
|
||||||
|
},
|
||||||
|
cal.get(Calendar.YEAR),
|
||||||
|
cal.get(Calendar.MONTH),
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH)
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
et.setOnClickListener { openDateThenTime() }
|
||||||
|
et.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) openDateThenTime() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Validasi Form Dinamis (PAKAI ID JENIS)
|
||||||
|
// =========================
|
||||||
|
private fun validateDynamicForm(details: List<KomunikasiDetail>): Boolean {
|
||||||
|
val fields = details
|
||||||
|
.filter { it.is_aktif == 1 }
|
||||||
|
.sortedBy { it.urutan ?: Int.MAX_VALUE }
|
||||||
|
|
||||||
|
// clear error
|
||||||
|
fieldTilByKode.values.forEach { it.error = null }
|
||||||
|
listInputEtByKode.values.forEach { it.error = null }
|
||||||
|
lampiranTvErrorByKode.values.forEach {
|
||||||
|
it.text = ""
|
||||||
|
it.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
for (d in fields) {
|
||||||
|
if (d.is_wajib != 1) continue
|
||||||
|
|
||||||
|
val kode = d.kode ?: continue
|
||||||
|
val label = d.isian?.trim().orEmpty().ifBlank { "Field" }
|
||||||
|
val jenisId = d.id_jenis_isian ?: continue
|
||||||
|
val value = formValues[kode]
|
||||||
|
|
||||||
|
val valid = when (jenisId) {
|
||||||
|
4 -> value is Int // angka (default 0 valid)
|
||||||
|
5 -> value is Double // pecahan (default 0.00 valid)
|
||||||
|
2 -> (value as? String)?.isNotBlank() == true // tanggal
|
||||||
|
1 -> { // waktu yyyy-MM-dd HH:mm:ss
|
||||||
|
val s = (value as? String).orEmpty().trim()
|
||||||
|
s.isNotBlank() && runCatching {
|
||||||
|
sdfDateTime.isLenient = false
|
||||||
|
sdfDateTime.parse(s) != null
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
3 -> { // jam HH:mm:ss
|
||||||
|
val s = (value as? String).orEmpty().trim()
|
||||||
|
s.isNotBlank() && Regex("""^\d{2}:\d{2}:\d{2}$""").matches(s)
|
||||||
|
}
|
||||||
|
10 -> (value as? List<*>)?.isNotEmpty() == true // list
|
||||||
|
11 -> (value as? List<*>)?.isNotEmpty() == true // lampiran
|
||||||
|
0, 6 -> (value as? String)?.isNotBlank() == true // teks/string
|
||||||
|
else -> (value as? String)?.isNotBlank() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
when (jenisId) {
|
||||||
|
10 -> listInputEtByKode[kode]?.error = "Wajib diisi"
|
||||||
|
11 -> {
|
||||||
|
lampiranTvErrorByKode[kode]?.apply {
|
||||||
|
text = "Wajib lampirkan minimal 1 file"
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> fieldTilByKode[kode]?.error = "Wajib diisi"
|
||||||
|
}
|
||||||
|
showSnack("Field wajib belum diisi: $label")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Build komunikasiDetail payload (sesuai ID)
|
||||||
|
// =========================
|
||||||
|
private fun buildKomunikasiDetailPayload(details: List<KomunikasiDetail>): List<Map<String, Any?>> {
|
||||||
|
val fields = details
|
||||||
|
.filter { it.is_aktif == 1 }
|
||||||
|
.sortedBy { it.urutan ?: Int.MAX_VALUE }
|
||||||
|
|
||||||
|
return fields.mapNotNull { d ->
|
||||||
|
val kode = d.kode ?: return@mapNotNull null
|
||||||
|
val jenisId = d.id_jenis_isian ?: return@mapNotNull null
|
||||||
|
|
||||||
|
val rawValue = formValues[kode]
|
||||||
|
|
||||||
|
val isianForServer: Any? = when (jenisId) {
|
||||||
|
4 -> ((rawValue as? Int) ?: 0).toString()
|
||||||
|
|
||||||
|
5 -> {
|
||||||
|
val v = (rawValue as? Double) ?: 0.0
|
||||||
|
String.format(Locale.US, "%.2f", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> (rawValue as? String).orEmpty() // yyyy-MM-dd
|
||||||
|
|
||||||
|
1 -> (rawValue as? String).orEmpty() // yyyy-MM-dd HH:mm:ss
|
||||||
|
|
||||||
|
3 -> (rawValue as? String).orEmpty() // HH:mm:ss
|
||||||
|
|
||||||
|
10 -> { // list => JSON [{"text":"a"},...]
|
||||||
|
val items = rawValue as? List<*>
|
||||||
|
val arr = items.orEmpty()
|
||||||
|
.mapNotNull { it?.toString()?.trim() }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { mapOf("text" to it) }
|
||||||
|
Gson().toJson(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
11 -> {
|
||||||
|
// lampiran => isian metadata JSON (opsional), file fisiknya via multipart
|
||||||
|
val atts = (rawValue as? List<FormAttachment>).orEmpty()
|
||||||
|
val meta = atts.map { mapOf("name" to it.fileName, "mime" to it.mimeType) }
|
||||||
|
Gson().toJson(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
0, 6 -> rawValue?.toString() // teks (multiline) / string
|
||||||
|
else -> rawValue?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
mapOf(
|
||||||
|
"kode" to kode,
|
||||||
|
"id_jenis_isian" to jenisId,
|
||||||
|
"isian" to isianForServer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildJsonDataBody(
|
||||||
|
actionItem: AddActionItem,
|
||||||
|
selectedRecipients: List<Pegawai>,
|
||||||
|
komunikasiDetail: List<Map<String, Any?>>
|
||||||
|
): RequestBody {
|
||||||
|
val payload = mapOf(
|
||||||
|
"kode" to actionItem.id,
|
||||||
|
"tentang" to actionItem.idTentang,
|
||||||
|
"topic" to actionItem.idKomunikasi,
|
||||||
|
"uraian" to "",
|
||||||
|
"kepada" to selectedRecipients.map { it.kode },
|
||||||
|
"komunikasiDetail" to komunikasiDetail
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = Gson().toJson(payload)
|
||||||
|
return json.toRequestBody("text/plain".toMediaType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Lampiran -> Multipart
|
||||||
|
// =========================
|
||||||
|
private fun collectLampiranParts(): List<MultipartBody.Part> {
|
||||||
|
val parts = mutableListOf<MultipartBody.Part>()
|
||||||
|
formValues.forEach { (kodeDetail, v) ->
|
||||||
|
val atts = (v as? List<*>)?.filterIsInstance<FormAttachment>().orEmpty()
|
||||||
|
for (att in atts) {
|
||||||
|
val part = uriToMultipart(
|
||||||
|
uri = att.uri,
|
||||||
|
fileName = att.fileName,
|
||||||
|
mimeType = att.mimeType,
|
||||||
|
fieldName = "isian_$kodeDetail" // ✅ penting!
|
||||||
|
)
|
||||||
|
if (part != null) parts.add(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uriToMultipart(
|
||||||
|
uri: android.net.Uri,
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String,
|
||||||
|
fieldName: String
|
||||||
|
): MultipartBody.Part? {
|
||||||
|
return runCatching {
|
||||||
|
val input = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
|
||||||
|
val outFile = File(cacheDir, "${System.currentTimeMillis()}_$fileName")
|
||||||
|
outFile.outputStream().use { out ->
|
||||||
|
input.use { it.copyTo(out) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val reqBody = outFile.asRequestBody(mimeType.toMediaType())
|
||||||
|
MultipartBody.Part.createFormData(fieldName, fileName, reqBody)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readUriMeta(uri: android.net.Uri): FormAttachment? {
|
||||||
|
val cr = contentResolver
|
||||||
|
val mime = cr.getType(uri) ?: "application/octet-stream"
|
||||||
|
|
||||||
|
var name: String? = null
|
||||||
|
cr.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { c ->
|
||||||
|
if (c.moveToFirst()) {
|
||||||
|
name = c.getString(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val fileName = name ?: "attachment"
|
||||||
|
|
||||||
|
return FormAttachment(uri = uri, fileName = fileName, mimeType = mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Existing stuff
|
||||||
|
// =========================
|
||||||
|
private fun initData() {
|
||||||
|
val idTentang = actionItem.idTentang
|
||||||
|
val idTipeKomunikasi = actionItem.idKomunikasi
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddTemplateActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
APIMain.require().selectionServices.recipients(
|
||||||
|
Preferences.getAccessToken(this), idTipeKomunikasi, idTentang
|
||||||
|
).enqueue(object : Callback<ArrayList<Pegawai>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
response: Response<ArrayList<Pegawai>>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
|
||||||
|
// ✅ buang pengirim dari list
|
||||||
|
val myKode = userData.pegawai?.kode?.trim().orEmpty()
|
||||||
|
val filtered = body.filter { p ->
|
||||||
|
p.kode.trim() != myKode
|
||||||
|
}
|
||||||
|
|
||||||
|
recipientOptions.clear()
|
||||||
|
recipientOptions.addAll(filtered)
|
||||||
|
|
||||||
|
selectedRecipients.clear()
|
||||||
|
selectedRecipients.addAll(filtered)
|
||||||
|
renderRecipients()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddTemplateActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<ArrayList<Pegawai>>, t: Throwable) {
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
handleBackPress(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ibBack.setOnClickListener { handleBackPress(0) }
|
||||||
|
|
||||||
|
tvAddRecipient.setOnClickListener {
|
||||||
|
if (isNetworkAvailable(this)) {
|
||||||
|
showProgressDialog(true)
|
||||||
|
APIMain.require().selectionServices.allRecipient(
|
||||||
|
Preferences.getAccessToken(this)
|
||||||
|
).enqueue(object : Callback<ArrayList<Pegawai>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Pegawai>>,
|
||||||
|
response: Response<ArrayList<Pegawai>>
|
||||||
|
) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddTemplateActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
val filtered = body.filter { p ->
|
||||||
|
p.kode.trim() != userData.pegawai?.kode
|
||||||
|
}
|
||||||
|
|
||||||
|
showRecipientPickerDialog(ArrayList(filtered))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@AddTemplateActionActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<ArrayList<Pegawai>>, t: Throwable) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showProgressDialog(false)
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(android.R.id.content),
|
||||||
|
ContextCompat.getString(this@AddTemplateActionActivity, R.string.no_internet_message),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btSend.setOnClickListener {
|
||||||
|
val details = actionItem.komunikasi_detail.orEmpty()
|
||||||
|
if (details.isEmpty()) {
|
||||||
|
showSnack("Tidak ada form yang bisa dikirim")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateDynamicForm(details)) return@setOnClickListener
|
||||||
|
|
||||||
|
if (selectedRecipients.isEmpty()) {
|
||||||
|
selectedRecipients.add(Pegawai(
|
||||||
|
kode = "0000000000000000",
|
||||||
|
nama = "Genie",
|
||||||
|
outlet = null,
|
||||||
|
jabatan = null,
|
||||||
|
mulai_bekerja = "0000-00-000",
|
||||||
|
id_kelamin = "L",
|
||||||
|
outlets = null
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
val komunikasiDetail = buildKomunikasiDetailPayload(details)
|
||||||
|
val dataBody = buildJsonDataBody(actionItem, selectedRecipients, komunikasiDetail)
|
||||||
|
val fileParts = collectLampiranParts()
|
||||||
|
|
||||||
|
APIMain.require().actionServices.add(
|
||||||
|
token = Preferences.getAccessToken(this),
|
||||||
|
data = dataBody,
|
||||||
|
files = fileParts
|
||||||
|
).enqueue(object : Callback<Message> {
|
||||||
|
override fun onResponse(call: Call<Message>, response: Response<Message>) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
showSnack(response.body()?.message ?: "Berhasil")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
showSnack(raw.ifBlank { "${response.code()} ${response.message()}" })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<Message>, t: Throwable) {
|
||||||
|
showSnack(t.message ?: "Gagal")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRecipients() {
|
||||||
|
val hasData = selectedRecipients.isNotEmpty()
|
||||||
|
rvRecipient.visibility = if (hasData) View.VISIBLE else View.GONE
|
||||||
|
tvEmpty.visibility = if (hasData) View.GONE else View.VISIBLE
|
||||||
|
recipientAdapter.submitList(selectedRecipients.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showRecipientPickerDialog(allRecipient: ArrayList<Pegawai>) {
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this@AddTemplateActionActivity),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
val cleaned = allRecipient.filter { it.kode.trim() != userData.pegawai?.kode }
|
||||||
|
if (cleaned.isEmpty()) {
|
||||||
|
showSnack("Daftar penerima kosong")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseList = cleaned.filter { ar ->
|
||||||
|
selectedRecipients.none { it.kode == ar.kode }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseList.isEmpty()) {
|
||||||
|
showSnack("Semua penerima sudah dipilih")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val v = layoutInflater.inflate(R.layout.dialog_recipient_picker, null)
|
||||||
|
val rv = v.findViewById<RecyclerView>(R.id.rv_recipient_picker)
|
||||||
|
val sv = v.findViewById<androidx.appcompat.widget.SearchView>(R.id.sv_recipient)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle("Pilih Penerima")
|
||||||
|
.setView(v)
|
||||||
|
.setNegativeButton("Tutup", null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val pickerAdapter = RecipientPickerAdapter { picked ->
|
||||||
|
val exists = selectedRecipients.any { it.kode == picked.kode }
|
||||||
|
if (exists) {
|
||||||
|
showSnack("Penerima sudah dipilih")
|
||||||
|
return@RecipientPickerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRecipients.add(picked)
|
||||||
|
renderRecipients()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.layoutManager = LinearLayoutManager(this)
|
||||||
|
rv.adapter = pickerAdapter
|
||||||
|
rv.setHasFixedSize(true)
|
||||||
|
|
||||||
|
pickerAdapter.submitList(baseList)
|
||||||
|
|
||||||
|
fun applyFilter(query: String?) {
|
||||||
|
val q = query.orEmpty().trim()
|
||||||
|
if (q.isEmpty()) {
|
||||||
|
pickerAdapter.submitList(baseList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val filtered = baseList.filter { p ->
|
||||||
|
p.nama.contains(q, ignoreCase = true) || p.kode.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pickerAdapter.submitList(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
sv.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
applyFilter(query)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
applyFilter(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
sv.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AttachmentPreviewActivity : BaseActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_attachment_preview)
|
||||||
|
|
||||||
|
val localPath = intent.getStringExtra("local_path").orEmpty()
|
||||||
|
val title = intent.getStringExtra("title").orEmpty()
|
||||||
|
|
||||||
|
val ib = findViewById<ImageButton>(R.id.ib_back_attachment_preview)
|
||||||
|
val iv = findViewById<ShapeableImageView>(R.id.iv_attachment_preview)
|
||||||
|
val tv = findViewById<TextView>(R.id.tv_title_attachment_preview)
|
||||||
|
|
||||||
|
tv.text = title.ifBlank { "Lampiran" }
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
handleBackPress(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ib.setOnClickListener { handleBackPress(0) }
|
||||||
|
|
||||||
|
// ✅ load dari file lokal
|
||||||
|
Glide.with(this)
|
||||||
|
.load(File(localPath))
|
||||||
|
.into(iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/src/main/java/com/amz/genie/activities/BaseActivity.kt
Normal file
120
app/src/main/java/com/amz/genie/activities/BaseActivity.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.ActivityOptions
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.amz.genie.R
|
||||||
|
|
||||||
|
open class BaseActivity: AppCompatActivity() {
|
||||||
|
|
||||||
|
private var showLoadingDialog: Dialog? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (showLoadingDialog == null) {
|
||||||
|
showLoadingDialog = Dialog(this)
|
||||||
|
showLoadingDialog!!.window!!.setBackgroundDrawable(0.toDrawable())
|
||||||
|
showLoadingDialog!!.setContentView(R.layout.loading_dialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFragment(fragment: Fragment) {
|
||||||
|
val transaction = supportFragmentManager.beginTransaction()
|
||||||
|
transaction.replace(R.id.frameLayout_Container, fragment)
|
||||||
|
transaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showProgressDialog(show: Boolean, type: Int = 1) {
|
||||||
|
when {
|
||||||
|
show -> {
|
||||||
|
if (!isFinishing) {
|
||||||
|
|
||||||
|
val lottie = showLoadingDialog
|
||||||
|
?.findViewById<LottieAnimationView>(R.id.lottieLoading)
|
||||||
|
|
||||||
|
val fileName = when (type) {
|
||||||
|
1 -> "lottie/loader_circle.json"
|
||||||
|
2 -> "lottie/loader_send.json"
|
||||||
|
else -> "lottie/loading.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
lottie?.apply {
|
||||||
|
setAnimation(fileName)
|
||||||
|
repeatCount = com.airbnb.lottie.LottieDrawable.INFINITE
|
||||||
|
playAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoadingDialog?.setCanceledOnTouchOutside(false)
|
||||||
|
if (showLoadingDialog?.isShowing != true) {
|
||||||
|
showLoadingDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
if (showLoadingDialog?.isShowing == true && !isFinishing) {
|
||||||
|
showLoadingDialog?.dismiss()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(target: Class<*>, enterAnim: Int, exitAnim: Int) {
|
||||||
|
startActivity(Intent(this@BaseActivity, target),
|
||||||
|
ActivityOptions.makeCustomAnimation(this@BaseActivity,
|
||||||
|
enterAnim, exitAnim).toBundle())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun arePushNotificationsEnabled(context: Context): Boolean {
|
||||||
|
val enabledByApp = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||||
|
|
||||||
|
if (!enabledByApp) return false
|
||||||
|
|
||||||
|
// Android 13+ also needs POST_NOTIFICATIONS permission
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchClick(listener: View.OnClickListener?, ibSearch: ImageView) {
|
||||||
|
ibSearch.setOnClickListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleBackPress(stat: Int = 0) {
|
||||||
|
if (stat == 5) {
|
||||||
|
navigateTo(LoginActivity::class.java, R.anim.right_in, R.anim.left_out)
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(R.anim.left_in, R.anim.right_out)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.adapters.GeneralDetailAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isFemale
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.models.GeneralDetailResponse
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class GeneralDetailActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var ivBack: ImageView
|
||||||
|
private lateinit var sivUser: ShapeableImageView
|
||||||
|
private lateinit var tvName: TextView
|
||||||
|
private lateinit var tvJobDesk: TextView
|
||||||
|
private lateinit var tvOutlet: TextView
|
||||||
|
private lateinit var rvgeneralDetail: RecyclerView
|
||||||
|
|
||||||
|
private lateinit var adapter: GeneralDetailAdapter
|
||||||
|
|
||||||
|
private val allItems = mutableListOf<GeneralThreadItem>()
|
||||||
|
private var currentPage = 1
|
||||||
|
private val perPage = 30
|
||||||
|
private var hasMore = true
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
private var counterpartKode: String? = null
|
||||||
|
private var childNeedsRefresh = false
|
||||||
|
|
||||||
|
private val subDetailLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val needsRefresh = result.data?.getBooleanExtra("needs_refresh", false) ?: false
|
||||||
|
if (result.resultCode == RESULT_OK && needsRefresh) {
|
||||||
|
childNeedsRefresh = true
|
||||||
|
refreshListFromFirstPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_general_detail)
|
||||||
|
|
||||||
|
initUI(savedInstanceState)
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(savedInstanceState: Bundle?) {
|
||||||
|
ivBack = findViewById(R.id.ib_back_general_detail)
|
||||||
|
sivUser = findViewById(R.id.siv_employee_general_detail)
|
||||||
|
tvName = findViewById(R.id.tv_employeename_general_detail)
|
||||||
|
tvJobDesk = findViewById(R.id.tv_jobdesk_general_detail)
|
||||||
|
tvOutlet = findViewById(R.id.tv_outlet_general_detail)
|
||||||
|
rvgeneralDetail = findViewById(R.id.rv_general_detail)
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this), Pengguna::class.java)
|
||||||
|
val myKodePegawai = userData.pegawai?.kode ?: ""
|
||||||
|
|
||||||
|
adapter = GeneralDetailAdapter(
|
||||||
|
kodePegawai = myKodePegawai,
|
||||||
|
onItemClick = { item ->
|
||||||
|
// ✅ klik item: mark as read dulu kalau unread, baru buka detail
|
||||||
|
markReadIfNeeded(item) { openSubDetail(item) }
|
||||||
|
},
|
||||||
|
onDetailClick = { item ->
|
||||||
|
// ✅ klik tombol detail juga sama
|
||||||
|
markReadIfNeeded(item) { openSubDetail(item) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
rvgeneralDetail.layoutManager = LinearLayoutManager(this)
|
||||||
|
rvgeneralDetail.adapter = adapter
|
||||||
|
rvgeneralDetail.setHasFixedSize(true)
|
||||||
|
|
||||||
|
if (intent.hasExtra("data")) {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bearerToken(): String {
|
||||||
|
val raw = Preferences.getAccessToken(this).orEmpty().trim()
|
||||||
|
// ✅ aman: kalau sudah ada "Bearer " jangan double
|
||||||
|
return if (raw.startsWith("Bearer ", true)) raw else "Bearer $raw"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSubDetail(item: GeneralThreadItem) {
|
||||||
|
val intent = Intent(this, GeneralSubDetailActivity::class.java)
|
||||||
|
val dataJson = Gson().toJson(item, GeneralThreadItem::class.java)
|
||||||
|
intent.putExtra("data", dataJson)
|
||||||
|
intent.putExtra("counterpart", counterpartKode)
|
||||||
|
subDetailLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (childNeedsRefresh) {
|
||||||
|
setResult(RESULT_OK, Intent().putExtra("needs_refresh", true))
|
||||||
|
}
|
||||||
|
handleBackPress(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ivBack.setOnClickListener {
|
||||||
|
if (childNeedsRefresh) {
|
||||||
|
setResult(RESULT_OK, Intent().putExtra("needs_refresh", true))
|
||||||
|
}
|
||||||
|
handleBackPress(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvgeneralDetail.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (dy <= 0) return
|
||||||
|
|
||||||
|
val lm = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
||||||
|
val totalItemCount = lm.itemCount
|
||||||
|
val lastVisible = lm.findLastVisibleItemPosition()
|
||||||
|
|
||||||
|
// trigger saat tinggal 5 item lagi
|
||||||
|
if (!isLoading && hasMore && totalItemCount > 0 && lastVisible >= totalItemCount - 5) {
|
||||||
|
loadPage(currentPage + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
val intentDataJson = intent.getStringExtra("data") ?: return
|
||||||
|
val data = Gson().fromJson(intentDataJson, GeneralThreadItem::class.java)
|
||||||
|
|
||||||
|
counterpartKode = data.counterpart
|
||||||
|
|
||||||
|
val tipe = data.tipe.uppercase()
|
||||||
|
val aksi = data.aksi
|
||||||
|
val reaksi = data.reaksi
|
||||||
|
|
||||||
|
if (tipe == "REAKSI" && reaksi != null) {
|
||||||
|
tvName.text = reaksi.pembuat.nama
|
||||||
|
tvJobDesk.text = reaksi.pembuat.jabatan?.nama ?: "-"
|
||||||
|
tvOutlet.text = reaksi.pembuat.outlet?.nama ?: "-"
|
||||||
|
|
||||||
|
val female = isFemale(reaksi.pembuat.id_kelamin)
|
||||||
|
sivUser.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
} else {
|
||||||
|
tvName.text = aksi.pembuat.nama
|
||||||
|
tvJobDesk.text = aksi.pembuat.jabatan?.nama ?: "-"
|
||||||
|
tvOutlet.text = aksi.pembuat.outlet?.nama ?: "-"
|
||||||
|
|
||||||
|
val female = isFemale(aksi.pembuat.id_kelamin)
|
||||||
|
sivUser.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNetworkAvailable(this)) {
|
||||||
|
showSnack(getString(R.string.no_internet_message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshListFromFirstPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshListFromFirstPage() {
|
||||||
|
allItems.clear()
|
||||||
|
adapter.submitRawTimeline(emptyList())
|
||||||
|
currentPage = 1
|
||||||
|
hasMore = true
|
||||||
|
isLoading = false
|
||||||
|
loadPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPage(page: Int) {
|
||||||
|
val cp = counterpartKode ?: run {
|
||||||
|
showSnack("counterpart_kode kosong")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return
|
||||||
|
if (!hasMore && page != 1) return
|
||||||
|
if (page <= currentPage && page != 1) return
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
showProgressDialog(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.detail(bearerToken(), cp, page, perPage)
|
||||||
|
.enqueue(object : Callback<GeneralDetailResponse> {
|
||||||
|
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<GeneralDetailResponse>,
|
||||||
|
response: Response<GeneralDetailResponse>
|
||||||
|
) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body()
|
||||||
|
val newItems = body?.items.orEmpty()
|
||||||
|
|
||||||
|
val meta = body?.meta
|
||||||
|
val metaPage = meta?.page
|
||||||
|
val metaHasMore = meta?.has_more
|
||||||
|
val metaNextPage = meta?.next_page
|
||||||
|
|
||||||
|
currentPage = metaPage ?: page
|
||||||
|
|
||||||
|
hasMore = when {
|
||||||
|
metaHasMore != null -> metaHasMore
|
||||||
|
metaNextPage != null -> true
|
||||||
|
else -> newItems.size >= perPage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == 1) allItems.clear()
|
||||||
|
|
||||||
|
// ✅ anti-duplikat (penting banget karena backend union bisa double kalau ada data ganda)
|
||||||
|
val existingKeys = allItems.map { keyOf(it) }.toHashSet()
|
||||||
|
val filtered = newItems.filter { existingKeys.add(keyOf(it)) }
|
||||||
|
|
||||||
|
allItems.addAll(filtered)
|
||||||
|
adapter.submitRawTimeline(allItems.toList())
|
||||||
|
|
||||||
|
if (allItems.isEmpty()) showSnack("Tidak ada pesan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<GeneralDetailResponse>, t: Throwable) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
isLoading = false
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyOf(item: GeneralThreadItem): String {
|
||||||
|
return if (item.tipe.equals("REAKSI", true)) {
|
||||||
|
"R_${item.reaksi?.id ?: -1}"
|
||||||
|
} else {
|
||||||
|
"A_${item.aksi.id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response<GeneralDetailResponse>) {
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@GeneralDetailActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MARK AS READ (FIX: REAKSI pakai reaksi.id, AKSI pakai aksi.id)
|
||||||
|
// ============================================================
|
||||||
|
private fun getReadPayload(item: GeneralThreadItem): Pair<Int, String>? {
|
||||||
|
return if (item.tipe.equals("REAKSI", true)) {
|
||||||
|
val rxId = item.reaksi?.id ?: return null
|
||||||
|
Pair(rxId, "REAKSI")
|
||||||
|
} else {
|
||||||
|
Pair(item.aksi.id, "AKSI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markReadIfNeeded(item: GeneralThreadItem, thenRun: () -> Unit) {
|
||||||
|
// backend GetgeneralDetail kamu ngirim is_unread / is_aktif
|
||||||
|
val isUnread = (item.is_unread == true) || ((item.is_aktif ?: 1) == 0)
|
||||||
|
if (!isUnread) {
|
||||||
|
thenRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val payload = getReadPayload(item)
|
||||||
|
if (payload == null) {
|
||||||
|
thenRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val (id, tipe) = payload
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.readed(bearerToken(), id, tipe)
|
||||||
|
.enqueue(object : Callback<Message> {
|
||||||
|
override fun onResponse(call: Call<Message>, response: Response<Message>) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
applyLocalReadState(id, tipe)
|
||||||
|
}
|
||||||
|
thenRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<Message>, t: Throwable) {
|
||||||
|
thenRun()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyLocalReadState(id: Int, tipe: String) {
|
||||||
|
val upper = tipe.uppercase()
|
||||||
|
var changed = false
|
||||||
|
|
||||||
|
for (i in allItems.indices) {
|
||||||
|
val it = allItems[i]
|
||||||
|
val match = if (upper == "REAKSI") {
|
||||||
|
it.tipe.equals("REAKSI", true) && it.reaksi?.id == id
|
||||||
|
} else {
|
||||||
|
!it.tipe.equals("REAKSI", true) && it.aksi.id == id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
allItems[i] = it.copy(
|
||||||
|
is_aktif = 1,
|
||||||
|
is_unread = false
|
||||||
|
)
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) adapter.submitRawTimeline(allItems.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,750 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.AutoCompleteTextView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.adapters.ChatAdapter
|
||||||
|
import com.amz.genie.helpers.EmojiPickerBottomSheet
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.models.ChatItem
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.InboxThreadResponse
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.models.RawMessage
|
||||||
|
import com.amz.genie.models.ReaksiPost
|
||||||
|
import com.amz.genie.models.ReaksiResponse
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class GeneralSubDetailActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var ivBack: ImageView
|
||||||
|
private lateinit var tvDescription: TextView
|
||||||
|
private lateinit var tvName: TextView
|
||||||
|
private lateinit var tvJobDesk: TextView
|
||||||
|
private lateinit var ibEmoji: ImageButton
|
||||||
|
private lateinit var ibAttach: ImageButton
|
||||||
|
private lateinit var ibCamera: ImageButton
|
||||||
|
private lateinit var ibMic: ImageButton
|
||||||
|
private lateinit var tietMessage: TextInputEditText
|
||||||
|
private lateinit var rvChat: RecyclerView
|
||||||
|
private lateinit var chatAdapter: ChatAdapter
|
||||||
|
private lateinit var lm: LinearLayoutManager
|
||||||
|
private lateinit var tvNewMsg: TextView
|
||||||
|
|
||||||
|
private var counterpartKode: String? = null
|
||||||
|
private var aksiId: Int? = null
|
||||||
|
private var reaksiId: Int? = null
|
||||||
|
private var tentangId: String = ""
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
private var hasMore = true
|
||||||
|
private var currentPage = 1
|
||||||
|
private val perPage = 30
|
||||||
|
private var isSendMode = false
|
||||||
|
private var newestMessageId: String? = null
|
||||||
|
|
||||||
|
private var userNearBottom = true
|
||||||
|
private var pendingNewCount = 0
|
||||||
|
|
||||||
|
// ✅ FIX READ PAYLOAD
|
||||||
|
private var readedId: Int? = null
|
||||||
|
private var readedTipe: String = "AKSI"
|
||||||
|
private var hasScheduledMark = false
|
||||||
|
private var hasPostedMark = false
|
||||||
|
private val markHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var markRunnable: Runnable? = null
|
||||||
|
|
||||||
|
private var displayTipe: String = "AKSI"
|
||||||
|
private var myKodePegawai: String = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_inbox_sub_detail)
|
||||||
|
|
||||||
|
initUI()
|
||||||
|
setupActions()
|
||||||
|
|
||||||
|
when {
|
||||||
|
intent.hasExtra("data") -> initData()
|
||||||
|
intent.hasExtra("aksi_id") && intent.hasExtra("counterpart_kode") -> initDataFromNotif()
|
||||||
|
else -> {
|
||||||
|
showSnack("Data notifikasi tidak lengkap")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDataFromNotif() {
|
||||||
|
counterpartKode = intent.getStringExtra("counterpart_kode")
|
||||||
|
aksiId = intent.getStringExtra("aksi_id")?.toIntOrNull()
|
||||||
|
tentangId = intent.getStringExtra("tentang_id").orEmpty()
|
||||||
|
|
||||||
|
if (counterpartKode.isNullOrBlank() || aksiId == null) {
|
||||||
|
showSnack("Data thread tidak lengkap")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this), Pengguna::class.java)
|
||||||
|
myKodePegawai = userData.pegawai?.kode.orEmpty()
|
||||||
|
|
||||||
|
displayTipe = (intent.getStringExtra("tipe") ?: "AKSI").uppercase()
|
||||||
|
readedTipe = "AKSI"
|
||||||
|
readedId = aksiId
|
||||||
|
|
||||||
|
currentPage = 1
|
||||||
|
hasMore = true
|
||||||
|
newestMessageId = null
|
||||||
|
pendingNewCount = 0
|
||||||
|
tvNewMsg.isVisible = false
|
||||||
|
chatAdapter.submitList(emptyList())
|
||||||
|
|
||||||
|
applyHeader(name = counterpartKode, jobDesk = "Memuat...", description = "")
|
||||||
|
loadOlder(1, scrollToBottom = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI() {
|
||||||
|
ivBack = findViewById(R.id.ib_back_inbox_sub_detail)
|
||||||
|
tvDescription = findViewById(R.id.tv_description_inbox_sub_detail)
|
||||||
|
tvName = findViewById(R.id.tv_name_inbox_sub_detail)
|
||||||
|
tvJobDesk = findViewById(R.id.tv_jobdesk_inbox_sub_detail)
|
||||||
|
ibEmoji = findViewById(R.id.ib_emoji_inbox_sub_detail)
|
||||||
|
ibAttach = findViewById(R.id.ib_attach_inbox_sub_detail)
|
||||||
|
ibCamera = findViewById(R.id.ib_camera_inbox_sub_detail)
|
||||||
|
ibMic = findViewById(R.id.ib_mic_inbox_sub_detail)
|
||||||
|
tietMessage = findViewById(R.id.tiet_message_inbox_sub_detail)
|
||||||
|
rvChat = findViewById(R.id.rv_inbox_sub_detail)
|
||||||
|
|
||||||
|
tvNewMsg = findViewById(R.id.tv_new_message_indicator)
|
||||||
|
tvNewMsg.isVisible = false
|
||||||
|
|
||||||
|
chatAdapter = ChatAdapter()
|
||||||
|
lm = LinearLayoutManager(this).apply { stackFromEnd = true }
|
||||||
|
|
||||||
|
rvChat.layoutManager = lm
|
||||||
|
rvChat.adapter = chatAdapter
|
||||||
|
rvChat.setHasFixedSize(true)
|
||||||
|
|
||||||
|
tietMessage.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) {
|
||||||
|
updateMicButtonByMessage()
|
||||||
|
}
|
||||||
|
override fun afterTextChanged(s: Editable?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateMicButtonByMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() = handleBackPress(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
ivBack.setOnClickListener { handleBackPress(0) }
|
||||||
|
|
||||||
|
ibEmoji.setOnClickListener {
|
||||||
|
EmojiPickerBottomSheet { emoji -> insertEmojiToMessage(emoji) }
|
||||||
|
.show(supportFragmentManager, "emoji_picker")
|
||||||
|
}
|
||||||
|
|
||||||
|
ibAttach.setOnClickListener { /* TODO */ }
|
||||||
|
ibCamera.setOnClickListener { /* TODO */ }
|
||||||
|
|
||||||
|
ibMic.setOnClickListener {
|
||||||
|
if (isSendMode) {
|
||||||
|
val msg = tietMessage.text?.toString()?.trim().orEmpty()
|
||||||
|
if (msg.isNotEmpty()) showSendOptionsDialog()
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rvChat.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
|
userNearBottom = isUserNearBottom()
|
||||||
|
|
||||||
|
if (userNearBottom && tvNewMsg.isVisible) {
|
||||||
|
pendingNewCount = 0
|
||||||
|
tvNewMsg.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstVisible = lm.findFirstVisibleItemPosition()
|
||||||
|
if (!isLoading && hasMore && firstVisible <= 3) {
|
||||||
|
loadOlder(currentPage + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tvNewMsg.setOnClickListener {
|
||||||
|
pendingNewCount = 0
|
||||||
|
tvNewMsg.isVisible = false
|
||||||
|
rvChat.scrollToPosition(chatAdapter.itemCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
val intentDataJson = intent.getStringExtra("data") ?: return
|
||||||
|
val data = Gson().fromJson(intentDataJson, GeneralThreadItem::class.java)
|
||||||
|
|
||||||
|
counterpartKode = intent.getStringExtra("counterpart")
|
||||||
|
displayTipe = data.tipe.uppercase()
|
||||||
|
|
||||||
|
val aksi = data.aksi
|
||||||
|
val reaksi = data.reaksi
|
||||||
|
|
||||||
|
aksiId = aksi.id
|
||||||
|
tentangId = aksi.tentang.id
|
||||||
|
reaksiId = if (displayTipe == "REAKSI" && reaksi != null) reaksi.id else null
|
||||||
|
|
||||||
|
readedTipe = displayTipe
|
||||||
|
readedId = if (readedTipe == "REAKSI") reaksiId else aksiId
|
||||||
|
|
||||||
|
if (displayTipe == "REAKSI" && reaksi != null) {
|
||||||
|
tvName.text = reaksi.pembuat.nama
|
||||||
|
tvJobDesk.text = "${reaksi.pembuat.jabatan?.nama} - ${reaksi.pembuat.outlet?.nama}"
|
||||||
|
tvDescription.text = reaksi.uraian
|
||||||
|
} else {
|
||||||
|
tvName.text = aksi.pembuat.nama
|
||||||
|
tvJobDesk.text = "${aksi.pembuat.jabatan?.nama} - ${aksi.pembuat.outlet?.nama}"
|
||||||
|
tvDescription.text = aksi.uraian
|
||||||
|
}
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(this), Pengguna::class.java)
|
||||||
|
myKodePegawai = userData.pegawai?.kode.orEmpty()
|
||||||
|
|
||||||
|
resetThreadStateAndLoad(scrollToBottom = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadOlder(page: Int, scrollToBottom: Boolean = false) {
|
||||||
|
val cp = counterpartKode ?: return
|
||||||
|
val idAksi = aksiId ?: return
|
||||||
|
if (isLoading) return
|
||||||
|
if (!hasMore && page != 1) return
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
showProgressDialog(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.threadDetail(
|
||||||
|
Preferences.getAccessToken(this@GeneralSubDetailActivity),
|
||||||
|
cp, idAksi, page, perPage
|
||||||
|
)
|
||||||
|
.enqueue(object : Callback<InboxThreadResponse> {
|
||||||
|
override fun onResponse(call: Call<InboxThreadResponse>, response: Response<InboxThreadResponse>) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@GeneralSubDetailActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
showSnack(message)
|
||||||
|
hasPostedMark = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body() ?: return
|
||||||
|
|
||||||
|
currentPage = body.meta?.page ?: page
|
||||||
|
hasMore = body.meta?.has_more ?: false
|
||||||
|
|
||||||
|
val raw = body.items.orEmpty() // newest -> oldest
|
||||||
|
if (page == 1) newestMessageId = raw.firstOrNull()?.id
|
||||||
|
|
||||||
|
val display = raw.asReversed() // oldest -> newest
|
||||||
|
|
||||||
|
if (page == 1) {
|
||||||
|
applyHeaderFromThread(display)
|
||||||
|
}
|
||||||
|
|
||||||
|
val chatItems = mapToChatItems(display, myKodePegawai)
|
||||||
|
|
||||||
|
if (page == 1) {
|
||||||
|
chatAdapter.submitList(chatItems)
|
||||||
|
if (scrollToBottom) rvChat.scrollToPosition(chatAdapter.itemCount - 1)
|
||||||
|
} else {
|
||||||
|
val beforeCount = chatAdapter.itemCount
|
||||||
|
val firstPos = lm.findFirstVisibleItemPosition()
|
||||||
|
val topView = rvChat.getChildAt(0)
|
||||||
|
val topOffset = topView?.top ?: 0
|
||||||
|
|
||||||
|
chatAdapter.prepend(chatItems)
|
||||||
|
lm.scrollToPositionWithOffset(
|
||||||
|
firstPos + (chatAdapter.itemCount - beforeCount),
|
||||||
|
topOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMore) scheduleMarkAfterFullyLoaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<InboxThreadResponse>, t: Throwable) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
isLoading = false
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapToChatItems(display: List<RawMessage>, myKode: String): List<ChatItem> {
|
||||||
|
val out = ArrayList<ChatItem>()
|
||||||
|
var prevSender: String? = null
|
||||||
|
|
||||||
|
for (m in display) {
|
||||||
|
val senderKode = m.sender_kode.trim()
|
||||||
|
if (senderKode.isEmpty()) continue
|
||||||
|
|
||||||
|
val sameAsPrev = prevSender != null && prevSender == senderKode
|
||||||
|
val isMine = senderKode == myKode
|
||||||
|
|
||||||
|
out.add(
|
||||||
|
ChatItem(
|
||||||
|
id = m.id.orEmpty(),
|
||||||
|
senderKode = senderKode,
|
||||||
|
senderName = m.sender?.nama,
|
||||||
|
senderJob = m.sender?.jabatan?.nama,
|
||||||
|
senderOutlet = m.sender?.outlet?.nama,
|
||||||
|
message = m.message,
|
||||||
|
timeText = m.waktu_buat.orEmpty(),
|
||||||
|
isMine = isMine,
|
||||||
|
isSameSenderAsPrev = sameAsPrev
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
prevSender = senderKode
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMicButtonByMessage() {
|
||||||
|
val hasText = !tietMessage.text.isNullOrBlank()
|
||||||
|
isSendMode = hasText
|
||||||
|
|
||||||
|
if (hasText) {
|
||||||
|
ibMic.setImageResource(R.drawable.send_24px)
|
||||||
|
ibMic.contentDescription = "send"
|
||||||
|
} else {
|
||||||
|
ibMic.setImageResource(R.drawable.mic_24px)
|
||||||
|
ibMic.contentDescription = "mic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertEmojiToMessage(emoji: String) {
|
||||||
|
val editable = tietMessage.text ?: return
|
||||||
|
val start = tietMessage.selectionStart.coerceAtLeast(0)
|
||||||
|
val end = tietMessage.selectionEnd.coerceAtLeast(0)
|
||||||
|
|
||||||
|
val minPos = minOf(start, end)
|
||||||
|
val maxPos = maxOf(start, end)
|
||||||
|
|
||||||
|
editable.replace(minPos, maxPos, emoji)
|
||||||
|
tietMessage.setSelection(minPos + emoji.length)
|
||||||
|
tietMessage.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUserNearBottom(threshold: Int = 2): Boolean {
|
||||||
|
val total = chatAdapter.itemCount
|
||||||
|
if (total == 0) return true
|
||||||
|
val lastVisible = lm.findLastVisibleItemPosition()
|
||||||
|
return lastVisible >= total - 1 - threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleMarkAfterFullyLoaded() {
|
||||||
|
if (hasScheduledMark || hasPostedMark) return
|
||||||
|
if (readedId == null) return
|
||||||
|
|
||||||
|
hasScheduledMark = true
|
||||||
|
markRunnable = Runnable {
|
||||||
|
postMarkToApi()
|
||||||
|
hasScheduledMark = false
|
||||||
|
}
|
||||||
|
markHandler.postDelayed(markRunnable!!, 1200L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postMarkToApi() {
|
||||||
|
if (hasPostedMark) return
|
||||||
|
|
||||||
|
val id = readedId ?: return
|
||||||
|
val tipeKirim = readedTipe
|
||||||
|
|
||||||
|
hasPostedMark = true
|
||||||
|
|
||||||
|
APIMain.require().generalServices.readed(
|
||||||
|
Preferences.getAccessToken(this),
|
||||||
|
id,
|
||||||
|
tipeKirim
|
||||||
|
).enqueue(object : Callback<Message> {
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<Message>, response: Response<Message>) {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
hasPostedMark = false
|
||||||
|
showSnack("${response.code()} ${response.message()}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, Intent().putExtra("needs_refresh", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<Message>, t: Throwable) {
|
||||||
|
hasPostedMark = false
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
markRunnable?.let { markHandler.removeCallbacks(it) }
|
||||||
|
hasScheduledMark = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
markRunnable?.let { markHandler.removeCallbacks(it) }
|
||||||
|
hasScheduledMark = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetThreadStateAndLoad(scrollToBottom: Boolean = true) {
|
||||||
|
currentPage = 1
|
||||||
|
hasMore = true
|
||||||
|
newestMessageId = null
|
||||||
|
pendingNewCount = 0
|
||||||
|
tvNewMsg.isVisible = false
|
||||||
|
chatAdapter.submitList(emptyList())
|
||||||
|
|
||||||
|
hasPostedMark = false
|
||||||
|
hasScheduledMark = false
|
||||||
|
|
||||||
|
loadOlder(1, scrollToBottom = scrollToBottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyHeader(name: String?, jobDesk: String?, description: String?) {
|
||||||
|
tvName.text = name ?: ""
|
||||||
|
tvJobDesk.text = jobDesk ?: ""
|
||||||
|
tvDescription.text = description ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyHeaderFromThread(display: List<RawMessage>) {
|
||||||
|
if (display.isEmpty()) return
|
||||||
|
|
||||||
|
val partnerMsg = display.firstOrNull { it.sender_kode.trim() != myKodePegawai }
|
||||||
|
?: display.lastOrNull { it.sender_kode.trim() != myKodePegawai }
|
||||||
|
|
||||||
|
val partner = partnerMsg?.sender
|
||||||
|
val partnerName = partner?.nama ?: (counterpartKode ?: "")
|
||||||
|
val partnerJob = listOfNotNull(partner?.jabatan?.nama, partner?.outlet?.nama).joinToString(" - ")
|
||||||
|
|
||||||
|
// ✅ ambil uraian kegiatan dari payload AKSI (message json), bukan dari text header aja
|
||||||
|
val desc = extractUraianKegiatanFromMsg(partnerMsg?.message)
|
||||||
|
|
||||||
|
tvName.text = partnerName
|
||||||
|
tvJobDesk.text = partnerJob
|
||||||
|
if (!desc.isNullOrBlank()) tvDescription.text = desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// SEND REAKSI (tetap)
|
||||||
|
// =============================
|
||||||
|
private fun showSendOptionsDialog() {
|
||||||
|
val msg = tietMessage.text?.toString()?.trim().orEmpty()
|
||||||
|
if (msg.isEmpty()) return
|
||||||
|
|
||||||
|
val options = listOf("Bertanya", "Informasi", "Laporan", "Pengajuan", "Penugasan")
|
||||||
|
|
||||||
|
val view = LayoutInflater.from(this).inflate(R.layout.dialog_send_type, null)
|
||||||
|
val til = view.findViewById<TextInputLayout>(R.id.til_type)
|
||||||
|
val actv = view.findViewById<AutoCompleteTextView>(R.id.actv_type_dialog_send_type)
|
||||||
|
|
||||||
|
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, options)
|
||||||
|
actv.setAdapter(adapter)
|
||||||
|
actv.setText(options[0], false)
|
||||||
|
|
||||||
|
actv.keyListener = null
|
||||||
|
actv.isCursorVisible = false
|
||||||
|
actv.isFocusable = true
|
||||||
|
actv.isFocusableInTouchMode = true
|
||||||
|
actv.showSoftInputOnFocus = false
|
||||||
|
actv.setOnClickListener { actv.showDropDown() }
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(this)
|
||||||
|
.setView(view)
|
||||||
|
.setNegativeButton("Batal", null)
|
||||||
|
.setPositiveButton("Kirim", null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
val selected = actv.text?.toString()?.trim().orEmpty()
|
||||||
|
if (selected.isEmpty()) {
|
||||||
|
til.error = "Pilih tipe komunikasi"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgressDialog(true)
|
||||||
|
|
||||||
|
val topic = when (selected) {
|
||||||
|
"Bertanya" -> "A"
|
||||||
|
"Informasi" -> "I"
|
||||||
|
"Laporan" -> "L"
|
||||||
|
"Approval" -> "P"
|
||||||
|
"Pengajuan" -> "R"
|
||||||
|
"Penugasan" -> "T"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val reaksi = ReaksiPost(
|
||||||
|
aksiId!!, "R",
|
||||||
|
reaksiId,
|
||||||
|
tentangId, topic, tietMessage.text.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
|
val json = gson.toJson(reaksi)
|
||||||
|
val body = json.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
|
|
||||||
|
APIMain.require().reaksiServices.add(
|
||||||
|
Preferences.getAccessToken(this@GeneralSubDetailActivity),
|
||||||
|
body, emptyList()
|
||||||
|
).enqueue(object : Callback<ReaksiResponse> {
|
||||||
|
override fun onResponse(call: Call<ReaksiResponse>, response: Response<ReaksiResponse>) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@GeneralSubDetailActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
showSnack(message)
|
||||||
|
hasPostedMark = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body()
|
||||||
|
val rawItems = body?.thread?.items
|
||||||
|
if (rawItems.isNullOrEmpty()) {
|
||||||
|
showSnack("Reaksi terkirim. Refresh thread...")
|
||||||
|
loadOlder(1, scrollToBottom = true)
|
||||||
|
til.error = null
|
||||||
|
tietMessage.setText("")
|
||||||
|
dialog.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val display = rawItems.asReversed()
|
||||||
|
val chatItems = mapToChatItems(display, myKodePegawai)
|
||||||
|
chatAdapter.submitList(chatItems)
|
||||||
|
rvChat.scrollToPosition(chatAdapter.itemCount - 1)
|
||||||
|
|
||||||
|
til.error = null
|
||||||
|
tietMessage.setText("")
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<ReaksiResponse>, t: Throwable) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
hasPostedMark = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractUraianKegiatanFromMsg(msg: com.google.gson.JsonObject?): String? {
|
||||||
|
if (msg == null) return null
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Safe helpers
|
||||||
|
// -------------------------
|
||||||
|
fun JsonElement?.asStringSafeNullable(): String? {
|
||||||
|
if (this == null || this.isJsonNull) return null
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
this.isJsonPrimitive -> this.asString
|
||||||
|
this.isJsonArray -> this.asJsonArray.joinToString("\n") { it.asStringSafeNullable().orEmpty() }
|
||||||
|
this.isJsonObject -> {
|
||||||
|
// common payload {nilai: "..."} / {value:"..."}
|
||||||
|
val o = this.asJsonObject
|
||||||
|
val pick = listOf("nilai", "value", "teks", "string", "uraian", "isi", "jawaban")
|
||||||
|
.firstNotNullOfOrNull { k -> o.get(k)?.takeIf { !it.isJsonNull } }
|
||||||
|
pick?.asStringSafeNullable() ?: o.toString()
|
||||||
|
}
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getObjAtPath(root: com.google.gson.JsonObject, path: String): JsonElement? {
|
||||||
|
var cur: JsonElement = root
|
||||||
|
for (seg in path.split(".")) {
|
||||||
|
if (!cur.isJsonObject) return null
|
||||||
|
val o = cur.asJsonObject
|
||||||
|
if (!o.has(seg)) return null
|
||||||
|
cur = o.get(seg)
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
|
||||||
|
fun arrFrom(vararg paths: String): com.google.gson.JsonArray? {
|
||||||
|
for (p in paths) {
|
||||||
|
val el = getObjAtPath(msg, p)
|
||||||
|
if (el != null && el.isJsonArray) return el.asJsonArray
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rootOrValuesGet(key: String): JsonElement? {
|
||||||
|
if (msg.has(key) && !msg.get(key).isJsonNull) return msg.get(key)
|
||||||
|
val values = msg.get("values")?.takeIf { it.isJsonObject }?.asJsonObject
|
||||||
|
if (values != null && values.has(key) && !values.get(key).isJsonNull) return values.get(key)
|
||||||
|
|
||||||
|
val dv = msg.get("detail_values")?.takeIf { it.isJsonObject }?.asJsonObject
|
||||||
|
if (dv != null && dv.has(key) && !dv.get(key).isJsonNull) return dv.get(key)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 1) cari definisi field "Uraian Kegiatan"
|
||||||
|
// -------------------------
|
||||||
|
val detailArr = arrFrom(
|
||||||
|
"komunikasi.komunikasi_detail",
|
||||||
|
"komunikasi_detail",
|
||||||
|
"aksi.komunikasi.komunikasi_detail",
|
||||||
|
"aksi.komunikasi.komunikasi_detail" // keep
|
||||||
|
) ?: return null
|
||||||
|
|
||||||
|
// target field = "Uraian Kegiatan" (kode detail 143) dan jenis teks (id_jenis_isian=0)
|
||||||
|
var kodeUraian: Int? = null
|
||||||
|
for (i in 0 until detailArr.size()) {
|
||||||
|
val el = detailArr[i]
|
||||||
|
if (!el.isJsonObject) continue
|
||||||
|
val d = el.asJsonObject
|
||||||
|
|
||||||
|
val kode = d.get("kode")?.takeIf { it.isJsonPrimitive }?.asInt
|
||||||
|
val jenisId =
|
||||||
|
d.get("id_jenis_isian")?.takeIf { it.isJsonPrimitive }?.asInt
|
||||||
|
?: d.get("jenis_isian")?.takeIf { it.isJsonObject }?.asJsonObject?.get("id")
|
||||||
|
?.takeIf { it.isJsonPrimitive }?.asInt
|
||||||
|
?: 0
|
||||||
|
|
||||||
|
val label = d.get("isian")?.asStringSafeNullable()?.trim().orEmpty()
|
||||||
|
|
||||||
|
if (jenisId == ChatAdapter.JENIS_TEKS) {
|
||||||
|
// paling ideal ketemu kode 143
|
||||||
|
if (kode == 143) {
|
||||||
|
kodeUraian = 143
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// fallback: label matching
|
||||||
|
if (kode != null && label.contains("uraian", true) && label.contains("kegiatan", true)) {
|
||||||
|
kodeUraian = kode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kalau tetap gak ketemu, fallback langsung 143
|
||||||
|
val kd = kodeUraian ?: 143
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 2) ambil value dari tempat yang benar
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
// A) jika backend sudah kirim map detail_values/values by kode detail
|
||||||
|
rootOrValuesGet(kd.toString())?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
rootOrValuesGet("nilai_$kd")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
rootOrValuesGet("isian_$kd")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
|
||||||
|
// B) pola sekarang: aksi_komunikasi_teks adalah object {id, aksi_id, nilai:"..."} TANPA kode_detail
|
||||||
|
// jadi ambil "nilai" dari aksi_komunikasi_teks kalau ada (karena untuk RK Harian biasanya cuma 1 teks yaitu uraian)
|
||||||
|
msg.get("aksi_komunikasi_teks")
|
||||||
|
?.asStringSafeNullable()
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { return it }
|
||||||
|
|
||||||
|
val teksObj = msg.get("aksi_komunikasi_teks")?.takeIf { it.isJsonObject }?.asJsonObject
|
||||||
|
teksObj?.get("nilai")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
|
||||||
|
// fallback lain
|
||||||
|
msg.get("aksi_komunikasi_string")?.takeIf { it.isJsonObject }?.asJsonObject
|
||||||
|
?.get("nilai")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Helpers Json Safe (single source)
|
||||||
|
// =========================
|
||||||
|
private fun JsonElement?.asStringSafeNullable(): String? {
|
||||||
|
if (this == null || this.isJsonNull) return null
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
this.isJsonPrimitive -> this.asString
|
||||||
|
this.isJsonArray -> this.asJsonArray.joinToString("\n") { it.asStringSafeNullable().orEmpty() }
|
||||||
|
this.isJsonObject -> this.toString()
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
325
app/src/main/java/com/amz/genie/activities/LoginActivity.kt
Normal file
325
app/src/main/java/com/amz/genie/activities/LoginActivity.kt
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.models.Login
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class LoginActivity : BaseActivity() {
|
||||||
|
private lateinit var tietUsername: TextInputEditText
|
||||||
|
private lateinit var tietPassword: TextInputEditText
|
||||||
|
private lateinit var tvForgotPassword: TextView
|
||||||
|
private lateinit var btLogin: Button
|
||||||
|
|
||||||
|
private lateinit var handler: Handler
|
||||||
|
private var backPressedTime: Long = 0
|
||||||
|
private val notificationPermission = Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String>
|
||||||
|
private var pendingUserData: Login? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
|
initUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI() {
|
||||||
|
tietUsername = findViewById(R.id.tiet_username_login)
|
||||||
|
tietPassword = findViewById(R.id.tiet_password_login)
|
||||||
|
tvForgotPassword = findViewById(R.id.tv_forgot_password_login)
|
||||||
|
btLogin = findViewById(R.id.bt_login)
|
||||||
|
|
||||||
|
handler = Handler(Looper.getMainLooper())
|
||||||
|
initializePermissionLaunchers()
|
||||||
|
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializePermissionLaunchers() {
|
||||||
|
// Notification permission launcher
|
||||||
|
notificationPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
// Notification permission granted
|
||||||
|
Log.d("NotificationPermission", "Push notification permission granted")
|
||||||
|
showCustomNotificationDialog(true)
|
||||||
|
} else {
|
||||||
|
// Notification permission denied
|
||||||
|
Log.w("NotificationPermission", "Push notification permission denied")
|
||||||
|
showCustomNotificationDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with login regardless of notification permission
|
||||||
|
pendingUserData?.let {
|
||||||
|
proceedAfterPermission(it)
|
||||||
|
} ?: run {
|
||||||
|
showProgressDialog(false)
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(android.R.id.content),
|
||||||
|
"Authentication error, please try again",
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
onBackPressedDispatcher.addCallback(this,
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (backPressedTime + 2000 > System.currentTimeMillis()) {
|
||||||
|
finishAffinity()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@LoginActivity,
|
||||||
|
"Tekan Sekali Lagi Untuk Keluar",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
backPressedTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
btLogin.setOnClickListener {
|
||||||
|
val username: String = tietUsername.text.toString().trim()
|
||||||
|
val password: String = tietPassword.text.toString().trim { it <= ' ' }
|
||||||
|
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
|
||||||
|
if (isNetworkAvailable(this)) {
|
||||||
|
showProgressDialog(true)
|
||||||
|
|
||||||
|
val paramObject = JSONObject()
|
||||||
|
paramObject.put("username", username)
|
||||||
|
paramObject.put("password", password)
|
||||||
|
|
||||||
|
val requestBody: RequestBody = paramObject.toString().toRequestBody(
|
||||||
|
"application/json".toMediaTypeOrNull())
|
||||||
|
|
||||||
|
val call: Call<Login> = APIMain.require().accountServices.login(requestBody)
|
||||||
|
call.enqueue(object: Callback<Login> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<Login?>,
|
||||||
|
response: Response<Login?>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val result: Login? = response.body()
|
||||||
|
result?.let {
|
||||||
|
pendingUserData = it
|
||||||
|
checkNotificationPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
val message = if (response.code() == 400) {
|
||||||
|
val errorJson = response.errorBody()?.string()
|
||||||
|
if (!errorJson.isNullOrEmpty()) {
|
||||||
|
JsonParser.parseString(errorJson).asJsonObject["message"]?.asString ?: "Bad Request"
|
||||||
|
} else {
|
||||||
|
"Bad Request"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
|
message,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
response.errorBody()?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<Login?>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
|
t.message.toString(),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
|
ContextCompat.getString(this@LoginActivity,
|
||||||
|
R.string.no_internet_message),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkNotificationPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, notificationPermission)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.d("NotificationPermission", "Showing notification explanation dialog")
|
||||||
|
showNotificationExplanationDialog()
|
||||||
|
} else {
|
||||||
|
Log.d("NotificationPermission", "Push notification permission already granted")
|
||||||
|
showCustomNotificationDialog(true)
|
||||||
|
pendingUserData?.let {
|
||||||
|
proceedAfterPermission(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d("NotificationPermission", "No notification permission required for Android < 13")
|
||||||
|
pendingUserData?.let {
|
||||||
|
proceedAfterPermission(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotificationExplanationDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.enable_push_notifications))
|
||||||
|
.setMessage(getString(R.string.notification_explanation))
|
||||||
|
.setPositiveButton(getString(R.string.allow_notifications)) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
Log.d("NotificationPermission", "Requesting notification permission")
|
||||||
|
notificationPermissionLauncher.launch(notificationPermission)
|
||||||
|
}
|
||||||
|
.setNegativeButton(getString(R.string.not_now)) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
Log.w("NotificationPermission", "User declined notification permission")
|
||||||
|
showCustomNotificationDialog(false)
|
||||||
|
pendingUserData?.let { proceedAfterPermission(it) }
|
||||||
|
}
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCustomNotificationDialog(isGranted: Boolean) {
|
||||||
|
if (isGranted) {
|
||||||
|
handler.post {
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(android.R.id.content),
|
||||||
|
"Notifikasi push sudah diaktifkan! Anda akan menerima pembaruan penting.",
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler.post {
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(android.R.id.content),
|
||||||
|
"Notifikasi dinonaktifkan. Anda bisa mengaktifkannya nanti di Pengaturan.",
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun proceedAfterPermission(userData: Login) {
|
||||||
|
showProgressDialog(false)
|
||||||
|
|
||||||
|
Preferences.setAccessToken(this@LoginActivity, "Bearer ${userData.access_token}")
|
||||||
|
Preferences.setRefreshToken(this@LoginActivity, "Bearer ${userData.refresh_token}")
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val data = gson.toJson(userData.user)
|
||||||
|
Preferences.setUserData(this@LoginActivity, data)
|
||||||
|
|
||||||
|
setupFCMAndLogin(userData)
|
||||||
|
|
||||||
|
pendingUserData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFCMAndLogin(userData: Login) {
|
||||||
|
val newTopic = userData.user.kode.toString().trim()
|
||||||
|
if (newTopic.isBlank()) {
|
||||||
|
Log.w("FCM", "newTopic blank, skip subscribe")
|
||||||
|
completeLoginProcess()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldTopic = Preferences.getLastFcmTopic(this).trim()
|
||||||
|
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener { tokenTask ->
|
||||||
|
if (tokenTask.isSuccessful) {
|
||||||
|
Log.d("FCM", "Token: ${tokenTask.result}")
|
||||||
|
} else {
|
||||||
|
Log.w("FCM", "Failed to get token", tokenTask.exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Unsubscribe old topic jika beda
|
||||||
|
val doSubscribeNew = {
|
||||||
|
FirebaseMessaging.getInstance().subscribeToTopic(newTopic)
|
||||||
|
.addOnCompleteListener { subTask ->
|
||||||
|
Log.d(
|
||||||
|
"FCM",
|
||||||
|
"subscribe topic=$newTopic success=${subTask.isSuccessful} err=${subTask.exception}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subTask.isSuccessful) {
|
||||||
|
Preferences.setLastFcmTopic(this, newTopic)
|
||||||
|
}
|
||||||
|
completeLoginProcess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTopic.isNotBlank() && oldTopic != newTopic) {
|
||||||
|
FirebaseMessaging.getInstance().unsubscribeFromTopic(oldTopic)
|
||||||
|
.addOnCompleteListener { unSubTask ->
|
||||||
|
Log.d(
|
||||||
|
"FCM",
|
||||||
|
"unsubscribe oldTopic=$oldTopic success=${unSubTask.isSuccessful} err=${unSubTask.exception}"
|
||||||
|
)
|
||||||
|
doSubscribeNew()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// old kosong atau sama -> langsung subscribe
|
||||||
|
doSubscribeNew()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun completeLoginProcess() {
|
||||||
|
showProgressDialog(false)
|
||||||
|
navigateTo(MainActivity::class.java, R.anim.right_in,
|
||||||
|
R.anim.left_out)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
321
app/src/main/java/com/amz/genie/activities/MainActivity.kt
Normal file
321
app/src/main/java/com/amz/genie/activities/MainActivity.kt
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.adapters.AddActionAdapter
|
||||||
|
import com.amz.genie.fragments.ArchiveFragment
|
||||||
|
import com.amz.genie.fragments.AssignedFragment
|
||||||
|
import com.amz.genie.fragments.DashboardFragment
|
||||||
|
import com.amz.genie.fragments.InboxFragment
|
||||||
|
import com.amz.genie.fragments.TODOFragment
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.models.AddActionItem
|
||||||
|
import com.amz.genie.models.Komunikasi
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class MainActivity : BaseActivity(), View.OnClickListener {
|
||||||
|
|
||||||
|
private var dashboardFragment = DashboardFragment()
|
||||||
|
private var inboxFragment = InboxFragment()
|
||||||
|
private var todoFragment = TODOFragment()
|
||||||
|
private var assignedFragment = AssignedFragment()
|
||||||
|
private var archiveFragment = ArchiveFragment()
|
||||||
|
|
||||||
|
private lateinit var llDashboard: LinearLayout
|
||||||
|
private lateinit var ivDashboard: ImageView
|
||||||
|
private lateinit var tvDashboard: TextView
|
||||||
|
private lateinit var llInbox: LinearLayout
|
||||||
|
private lateinit var ivInbox: ImageView
|
||||||
|
private lateinit var tvInbox: TextView
|
||||||
|
private lateinit var llTodo: LinearLayout
|
||||||
|
private lateinit var ivTodo: ImageView
|
||||||
|
private lateinit var tvTodo: TextView
|
||||||
|
private lateinit var llAssigned: LinearLayout
|
||||||
|
private lateinit var ivAssigned: ImageView
|
||||||
|
private lateinit var tvAssigned: TextView
|
||||||
|
private lateinit var llArchive: LinearLayout
|
||||||
|
private lateinit var ivArchive: ImageView
|
||||||
|
private lateinit var tvArchive: TextView
|
||||||
|
lateinit var ibSearch: ImageButton
|
||||||
|
lateinit var ibAdd: ImageButton
|
||||||
|
lateinit var ibMenu: ImageButton
|
||||||
|
|
||||||
|
private var layouts: Array<LinearLayout>? = null
|
||||||
|
private var imageViews: Array<ImageView>? = null
|
||||||
|
private var textViews: Array<TextView>? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
initUI(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(savedInstanceState: Bundle?) {
|
||||||
|
llDashboard = findViewById(R.id.ll_dashboard_bottom_navigation)
|
||||||
|
ivDashboard = findViewById(R.id.iv_dashboard_bottom_navigation)
|
||||||
|
tvDashboard = findViewById(R.id.tv_dashboard_bottom_navigation)
|
||||||
|
llInbox = findViewById(R.id.ll_inbox_bottom_navigation)
|
||||||
|
ivInbox = findViewById(R.id.iv_inbox_bottom_navigation)
|
||||||
|
tvInbox = findViewById(R.id.tv_inbox_bottom_navigation)
|
||||||
|
llTodo = findViewById(R.id.ll_todo_bottom_navigation)
|
||||||
|
ivTodo = findViewById(R.id.iv_todo_bottom_navigation)
|
||||||
|
tvTodo = findViewById(R.id.tv_todo_bottom_navigation)
|
||||||
|
llAssigned = findViewById(R.id.ll_assigned_bottom_navigation)
|
||||||
|
ivAssigned = findViewById(R.id.iv_assigned_bottom_navigation)
|
||||||
|
tvAssigned = findViewById(R.id.tv_assigned_bottom_navigation)
|
||||||
|
llArchive = findViewById(R.id.ll_archive_bottom_navigation)
|
||||||
|
ivArchive = findViewById(R.id.iv_archive_bottom_navigation)
|
||||||
|
tvArchive = findViewById(R.id.tv_archive_bottom_navigation)
|
||||||
|
ibSearch = findViewById(R.id.ib_search_main)
|
||||||
|
ibAdd = findViewById(R.id.ib_add_aksi_main)
|
||||||
|
ibMenu = findViewById(R.id.ib_settings_main)
|
||||||
|
|
||||||
|
layouts = arrayOf(llDashboard, llInbox, llTodo, llAssigned, llArchive)
|
||||||
|
imageViews = arrayOf(ivDashboard, ivInbox, ivTodo, ivAssigned, ivArchive)
|
||||||
|
textViews = arrayOf(tvDashboard, tvInbox, tvTodo, tvAssigned, tvArchive)
|
||||||
|
|
||||||
|
layouts?.forEach {
|
||||||
|
it.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
dashboardFragment = DashboardFragment()
|
||||||
|
inboxFragment = InboxFragment()
|
||||||
|
todoFragment = TODOFragment()
|
||||||
|
assignedFragment = AssignedFragment()
|
||||||
|
archiveFragment = ArchiveFragment()
|
||||||
|
loadFragment(inboxFragment)
|
||||||
|
ibSearch.visibility = View.VISIBLE
|
||||||
|
setSelected(llInbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
ibAdd.setOnClickListener { showAddActionDialog() }
|
||||||
|
ibMenu.setOnClickListener {
|
||||||
|
val intent = Intent(this, MoreActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableTab(linearLayout: LinearLayout, imageView: ImageView, textView: TextView) {
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
params.weight = 0.8f
|
||||||
|
params.gravity= Gravity.CENTER
|
||||||
|
linearLayout.layoutParams = params
|
||||||
|
linearLayout.background = null
|
||||||
|
textView.visibility = View.GONE
|
||||||
|
imageView.background=null
|
||||||
|
|
||||||
|
imageView.setColorFilter(
|
||||||
|
ContextCompat.getColor(this, R.color.textColorSecondary),
|
||||||
|
PorterDuff.Mode.MULTIPLY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enable(linearLayout: LinearLayout, imageView: ImageView, textView: TextView) {
|
||||||
|
linearLayout.background = ContextCompat.getDrawable(this, R.drawable.bg_bottom)
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
params.weight = 0.8f
|
||||||
|
params.gravity= Gravity.CENTER
|
||||||
|
linearLayout.layoutParams = params
|
||||||
|
imageView.setColorFilter(
|
||||||
|
ContextCompat.getColor(this,
|
||||||
|
R.color.colorBlack
|
||||||
|
))
|
||||||
|
textView.visibility = View.VISIBLE
|
||||||
|
textView.setTextColor(ContextCompat.getColor(this, R.color.colorPrimaryDark))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSelected(mBarImg: LinearLayout) {
|
||||||
|
for ((index, linearLayout) in this.layouts!!.withIndex()) {
|
||||||
|
if (linearLayout === mBarImg) {
|
||||||
|
enable(linearLayout, this.imageViews?.get(index)!!, textViews?.get(index)!!)
|
||||||
|
} else {
|
||||||
|
disableTab(linearLayout, imageViews?.get(index)!!, textViews?.get(index)!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(p0: View?) {
|
||||||
|
when (p0!!.id) {
|
||||||
|
R.id.ll_dashboard_bottom_navigation -> {
|
||||||
|
loadFragment(dashboardFragment)
|
||||||
|
setSelected(llDashboard)
|
||||||
|
|
||||||
|
ibSearch.visibility = View.GONE
|
||||||
|
}
|
||||||
|
R.id.ll_inbox_bottom_navigation -> {
|
||||||
|
loadFragment(inboxFragment)
|
||||||
|
setSelected(llInbox)
|
||||||
|
|
||||||
|
ibSearch.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
R.id.ll_todo_bottom_navigation -> {
|
||||||
|
loadFragment(todoFragment)
|
||||||
|
setSelected(llTodo)
|
||||||
|
|
||||||
|
ibSearch.visibility = View.GONE
|
||||||
|
}
|
||||||
|
R.id.ll_assigned_bottom_navigation -> {
|
||||||
|
loadFragment(assignedFragment)
|
||||||
|
setSelected(llAssigned)
|
||||||
|
|
||||||
|
ibSearch.visibility = View.GONE
|
||||||
|
}
|
||||||
|
R.id.ll_archive_bottom_navigation -> {
|
||||||
|
loadFragment(archiveFragment)
|
||||||
|
setSelected(llArchive)
|
||||||
|
|
||||||
|
ibSearch.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (Preferences.getUserData(this) != null) { // hanya kalau sudah login
|
||||||
|
if (!arePushNotificationsEnabled(this)) {
|
||||||
|
showNotificationDisabledPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotificationDisabledPrompt() {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
|
"Notifikasi sedang nonaktif. Aktifkan agar dapat pembaruan penting.",
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).setAction("Pengaturan") {
|
||||||
|
openAppNotificationSettings()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAppNotificationSettings() {
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||||
|
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAddActionDialog() {
|
||||||
|
val dialogView = layoutInflater.inflate(R.layout.dialog_add_action, null)
|
||||||
|
val rv = dialogView.findViewById<RecyclerView>(R.id.rvActions)
|
||||||
|
|
||||||
|
val alertDialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||||
|
.setView(dialogView)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
// 1) Siapkan list mutable, isi dulu id=0
|
||||||
|
val items = mutableListOf(
|
||||||
|
AddActionItem(0, "Custom")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2) Pasang adapter dari awal biar dialog bisa tampil, lalu nanti update saat API selesai
|
||||||
|
val adapter = AddActionAdapter(items) { item ->
|
||||||
|
alertDialog.dismiss()
|
||||||
|
when (item.id) {
|
||||||
|
0 -> { // Custom
|
||||||
|
startActivity(Intent(this@MainActivity,
|
||||||
|
AddCustomActionActivity::class.java))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val intent = Intent(this@MainActivity,
|
||||||
|
AddTemplateActionActivity::class.java)
|
||||||
|
val data = Gson().toJson(item)
|
||||||
|
intent.putExtra("data", data)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.layoutManager = LinearLayoutManager(this)
|
||||||
|
rv.adapter = adapter
|
||||||
|
|
||||||
|
alertDialog.show()
|
||||||
|
|
||||||
|
// 3) Ambil data dari API, tambahkan ke list, lalu urutkan dari id=0
|
||||||
|
APIMain.require().selectionServices
|
||||||
|
.komunikasi(Preferences.getAccessToken(this))
|
||||||
|
.enqueue(object : Callback<ArrayList<Komunikasi>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ArrayList<Komunikasi>>,
|
||||||
|
response: Response<ArrayList<Komunikasi>>
|
||||||
|
) {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(this@MainActivity)
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
response.errorBody()?.close()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
|
||||||
|
// tambahkan hasil API ke list
|
||||||
|
body.forEach { k ->
|
||||||
|
items.add(AddActionItem(k.kode, k.komunikasi,
|
||||||
|
k.id_tentang, k.id_tipe_komunikasi,
|
||||||
|
k.komunikasi_detail))
|
||||||
|
}
|
||||||
|
|
||||||
|
// urutkan dari id paling kecil (custom id=0 otomatis di atas)
|
||||||
|
items.sortBy { it.id }
|
||||||
|
|
||||||
|
// refresh adapter (kalau adapter kamu belum punya method update, lihat catatan di bawah)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<ArrayList<Komunikasi>>, t: Throwable) {
|
||||||
|
showSnack(t.message.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
app/src/main/java/com/amz/genie/activities/MoreActivity.kt
Normal file
98
app/src/main/java/com/amz/genie/activities/MoreActivity.kt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isFemale
|
||||||
|
import com.amz.genie.helpers.Utils.uriToBase64
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.gson.Gson
|
||||||
|
|
||||||
|
class MoreActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var ibBack: ImageButton
|
||||||
|
private lateinit var ibBrowsePicture: ImageButton
|
||||||
|
private lateinit var btLogout: Button
|
||||||
|
private lateinit var sivEmployee: ShapeableImageView
|
||||||
|
private lateinit var tvName: TextView
|
||||||
|
private lateinit var tvJobDesk: TextView
|
||||||
|
private lateinit var pickImageLauncher: ActivityResultLauncher<String>
|
||||||
|
private var base64Picture = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_more)
|
||||||
|
|
||||||
|
initUI()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI() {
|
||||||
|
ibBack = findViewById(R.id.ib_back_more)
|
||||||
|
ibBrowsePicture = findViewById(R.id.ib_edit_picture_more)
|
||||||
|
btLogout = findViewById(R.id.bt_logout_more)
|
||||||
|
sivEmployee = findViewById(R.id.siv_employee_more)
|
||||||
|
tvJobDesk = findViewById(R.id.tv_jobdesk_more)
|
||||||
|
tvName = findViewById(R.id.tv_name_more)
|
||||||
|
|
||||||
|
initData()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
ibBack.setOnClickListener { handleBackPress(0) }
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() = handleBackPress(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||||
|
uri?.let {
|
||||||
|
sivEmployee.setImageURI(it)
|
||||||
|
base64Picture = uriToBase64(this, it).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btLogout.setOnClickListener {
|
||||||
|
showLogoutDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
ibBrowsePicture.setOnClickListener {
|
||||||
|
pickImageLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
val data = Gson().fromJson(Preferences.getUserData(this),
|
||||||
|
Pengguna::class.java)
|
||||||
|
|
||||||
|
tvName.text = data.pegawai?.nama
|
||||||
|
tvJobDesk.text = data.pegawai?.jabatan?.nama
|
||||||
|
val female = isFemale(data.pegawai?.id_kelamin)
|
||||||
|
sivEmployee.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLogoutDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Logout")
|
||||||
|
.setMessage("Yakin ingin keluar dari akun ini?")
|
||||||
|
.setNegativeButton("Batal") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setPositiveButton("Logout") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
forceLogoutAndGoLogin(this@MoreActivity)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/src/main/java/com/amz/genie/activities/SplashActivity.kt
Normal file
39
app/src/main/java/com/amz/genie/activities/SplashActivity.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.amz.genie.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("CustomSplashScreen")
|
||||||
|
class SplashActivity : BaseActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_splash)
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
if (isNetworkAvailable(this)) {
|
||||||
|
if (Preferences.getUserData(this) == null) {
|
||||||
|
navigateTo(LoginActivity::class.java,
|
||||||
|
R.anim.right_in, R.anim.left_out)
|
||||||
|
finish()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
navigateTo(MainActivity::class.java,
|
||||||
|
R.anim.right_in, R.anim.left_out)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/src/main/java/com/amz/genie/adapters/ActivityAdapter.kt
Normal file
55
app/src/main/java/com/amz/genie/adapters/ActivityAdapter.kt
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.models.ActivityItem
|
||||||
|
|
||||||
|
class ActivityAdapter(
|
||||||
|
private val onDelete: (Int) -> Unit
|
||||||
|
) : RecyclerView.Adapter<ActivityAdapter.VH>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<ActivityItem>()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<ActivityItem>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItem(item: ActivityItem) {
|
||||||
|
items.add(item)
|
||||||
|
notifyItemInserted(items.lastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAt(position: Int) {
|
||||||
|
if (position < 0 || position >= items.size) return
|
||||||
|
items.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItems(): List<ActivityItem> = items.toList()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_activity, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(items[position], position)
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tv = itemView.findViewById<TextView>(R.id.tv_activity)
|
||||||
|
private val btnDelete = itemView.findViewById<ImageButton>(R.id.btn_delete_activity)
|
||||||
|
|
||||||
|
fun bind(item: ActivityItem, pos: Int) {
|
||||||
|
tv.text = item.text
|
||||||
|
btnDelete.setOnClickListener { onDelete(pos) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/src/main/java/com/amz/genie/adapters/AddActionAdapter.kt
Normal file
33
app/src/main/java/com/amz/genie/adapters/AddActionAdapter.kt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.models.AddActionItem
|
||||||
|
|
||||||
|
class AddActionAdapter(
|
||||||
|
private val items: List<AddActionItem>,
|
||||||
|
private val onClick: (AddActionItem) -> Unit
|
||||||
|
) : androidx.recyclerview.widget.RecyclerView.Adapter<AddActionAdapter.VH>() {
|
||||||
|
|
||||||
|
inner class VH(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
|
||||||
|
val tvName: TextView = itemView.findViewById(R.id.tv_name_item_add_action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_add_action, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
holder.tvName.text = item.title
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener { onClick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.models.AttachmentItem
|
||||||
|
|
||||||
|
class AttachmentAdapter(
|
||||||
|
private val onRemove: (AttachmentItem) -> Unit
|
||||||
|
) : RecyclerView.Adapter<AttachmentAdapter.VH>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<AttachmentItem>()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<AttachmentItem>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_attachment_chip, parent, false)
|
||||||
|
return VH(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tvName: TextView = itemView.findViewById(R.id.tv_name_attachment_chip)
|
||||||
|
private val ibRemove: ImageButton = itemView.findViewById(R.id.ib_remove_attachment_chip)
|
||||||
|
|
||||||
|
fun bind(item: AttachmentItem) {
|
||||||
|
tvName.text = item.name
|
||||||
|
ibRemove.setOnClickListener { onRemove(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
637
app/src/main/java/com/amz/genie/adapters/ChatAdapter.kt
Normal file
637
app/src/main/java/com/amz/genie/adapters/ChatAdapter.kt
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.AttachmentPreviewActivity
|
||||||
|
import com.amz.genie.helpers.AttachmentDownloader
|
||||||
|
import com.amz.genie.helpers.AttachmentExtractor
|
||||||
|
import com.amz.genie.helpers.ChatMessageRenderer
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Preferences.API_URL
|
||||||
|
import com.amz.genie.models.ChatItem
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class ChatAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VT_LEFT = 1
|
||||||
|
private const val VT_RIGHT = 2
|
||||||
|
|
||||||
|
const val JENIS_TEKS = 0
|
||||||
|
const val JENIS_WAKTU = 1
|
||||||
|
const val JENIS_TANGGAL = 2
|
||||||
|
const val JENIS_JAM = 3
|
||||||
|
const val JENIS_ANGKA = 4
|
||||||
|
const val JENIS_PECAHAN = 5
|
||||||
|
const val JENIS_STRING = 6
|
||||||
|
const val JENIS_LIST = 10
|
||||||
|
const val JENIS_LAMPIRAN = 11
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SimpleAtt(val url: String, val fileName: String, val label: String = "Lampiran")
|
||||||
|
private val items = mutableListOf<ChatItem>()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<ChatItem>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepend(oldItems: List<ChatItem>) {
|
||||||
|
if (oldItems.isEmpty()) return
|
||||||
|
items.addAll(0, oldItems)
|
||||||
|
notifyItemRangeInserted(0, oldItems.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFirstVisibleId(): String? = items.firstOrNull()?.id
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int =
|
||||||
|
if (items[position].isMine) VT_RIGHT else VT_LEFT
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val inf = LayoutInflater.from(parent.context)
|
||||||
|
return if (viewType == VT_RIGHT) {
|
||||||
|
RightVH(inf.inflate(R.layout.item_chat_right, parent, false))
|
||||||
|
} else {
|
||||||
|
LeftVH(inf.inflate(R.layout.item_chat_left, parent, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
when (holder) {
|
||||||
|
is LeftVH -> holder.bind(item)
|
||||||
|
is RightVH -> holder.bind(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// ViewHolders
|
||||||
|
// =========================
|
||||||
|
inner class LeftVH(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
|
private val tvHeader = v.findViewById<TextView>(R.id.tv_header_item_chat_left)
|
||||||
|
private val tvMessage = v.findViewById<TextView>(R.id.tv_message_item_chat_left)
|
||||||
|
private val tvTime = v.findViewById<TextView>(R.id.tv_time_item_chat_left)
|
||||||
|
private val ivAvatar = v.findViewById<ShapeableImageView>(R.id.iv_avatar_item_chat_left)
|
||||||
|
private val llAttachments = v.findViewById<LinearLayout>(R.id.ll_attachments_item_chat_left)
|
||||||
|
private val llDetail = v.findViewById<LinearLayout>(R.id.ll_detail_item_chat_left)
|
||||||
|
|
||||||
|
fun bind(item: ChatItem) {
|
||||||
|
tvMessage.text = ChatMessageRenderer.render(item.message)
|
||||||
|
tvTime.text = item.timeText ?: ""
|
||||||
|
|
||||||
|
bindKomunikasiDetail(llDetail, item.message)
|
||||||
|
|
||||||
|
val headerText = buildString {
|
||||||
|
append(item.senderName ?: "")
|
||||||
|
val jobOutlet = listOfNotNull(item.senderJob, item.senderOutlet).joinToString(" - ")
|
||||||
|
if (jobOutlet.isNotBlank()) {
|
||||||
|
append(" • ")
|
||||||
|
append(jobOutlet)
|
||||||
|
}
|
||||||
|
}.trim()
|
||||||
|
|
||||||
|
val showMeta = !item.isSameSenderAsPrev
|
||||||
|
tvHeader.isVisible = showMeta
|
||||||
|
ivAvatar.isVisible = showMeta
|
||||||
|
tvHeader.text = headerText.ifBlank { "-" }
|
||||||
|
|
||||||
|
bindAttachments(llAttachments, item.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class RightVH(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
|
private val tvMessage = v.findViewById<TextView>(R.id.tv_message_item_chat_right)
|
||||||
|
private val tvTime = v.findViewById<TextView>(R.id.tv_time_item_chat_right)
|
||||||
|
private val llAttachments = v.findViewById<LinearLayout>(R.id.ll_attachments_item_chat_right)
|
||||||
|
private val llDetail = v.findViewById<LinearLayout>(R.id.ll_detail_item_chat_right)
|
||||||
|
|
||||||
|
fun bind(item: ChatItem) {
|
||||||
|
tvMessage.text = ChatMessageRenderer.render(item.message)
|
||||||
|
tvTime.text = item.timeText ?: ""
|
||||||
|
bindKomunikasiDetail(llDetail, item.message)
|
||||||
|
bindAttachments(llAttachments, item.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Attachments
|
||||||
|
// =========================
|
||||||
|
private fun fallbackExtractAttachments(msg: JsonObject?): List<SimpleAtt> {
|
||||||
|
if (msg == null) return emptyList()
|
||||||
|
|
||||||
|
fun arr(vararg keys: String) = keys.firstNotNullOfOrNull { k ->
|
||||||
|
msg.get(k)?.takeIf { it.isJsonArray }?.asJsonArray
|
||||||
|
}
|
||||||
|
|
||||||
|
val a = arr("attachments", "lampiran", "files", "berkas") ?: return emptyList()
|
||||||
|
|
||||||
|
val out = ArrayList<SimpleAtt>()
|
||||||
|
for (i in 0 until a.size()) {
|
||||||
|
val it = a[i]
|
||||||
|
if (!it.isJsonObject) continue
|
||||||
|
val o = it.asJsonObject
|
||||||
|
|
||||||
|
val url =
|
||||||
|
o.get("url")?.asStringSafe()
|
||||||
|
?: o.get("file_url")?.asStringSafe()
|
||||||
|
?: o.get("path")?.asStringSafe()
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
val name =
|
||||||
|
o.get("fileName")?.asStringSafe()
|
||||||
|
?: o.get("filename")?.asStringSafe()
|
||||||
|
?: o.get("name")?.asStringSafe()
|
||||||
|
?: o.get("nama_file")?.asStringSafe()
|
||||||
|
?: "Attachment"
|
||||||
|
|
||||||
|
if (url.isNotBlank()) out.add(SimpleAtt(url = url, fileName = name))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractAksiKomunikasiLampiran(msg: JsonObject?): List<SimpleAtt> {
|
||||||
|
if (msg == null) return emptyList()
|
||||||
|
val el = msg.get("aksi_komunikasi_lampiran") ?: return emptyList()
|
||||||
|
|
||||||
|
val urls = mutableListOf<String>()
|
||||||
|
|
||||||
|
fun push(u: String) {
|
||||||
|
val s = u.trim()
|
||||||
|
if (s.isBlank()) return
|
||||||
|
urls.add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
when {
|
||||||
|
// ✅ format kamu sekarang: object { "weirdKey": [ "path1", ... ], "weirdKey2": [..] }
|
||||||
|
el.isJsonObject -> {
|
||||||
|
val o = el.asJsonObject
|
||||||
|
|
||||||
|
// case normal: {nilai:[...]} (kalau suatu saat kamu rapihin backend)
|
||||||
|
val nilai = o.get("nilai")
|
||||||
|
if (nilai != null) {
|
||||||
|
if (nilai.isJsonArray) {
|
||||||
|
for (i in 0 until nilai.asJsonArray.size()) push(nilai.asJsonArray[i].asStringSafe())
|
||||||
|
} else if (nilai.isJsonPrimitive) push(nilai.asString)
|
||||||
|
} else {
|
||||||
|
// case weird: ambil semua entry yang value-nya JsonArray
|
||||||
|
for ((_, v) in o.entrySet()) {
|
||||||
|
if (!v.isJsonArray) continue
|
||||||
|
val arr = v.asJsonArray
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
push(arr[i].asStringSafe())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback kalau jadi array
|
||||||
|
el.isJsonArray -> {
|
||||||
|
val arr = el.asJsonArray
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
val it = arr[i]
|
||||||
|
if (it.isJsonPrimitive) push(it.asString)
|
||||||
|
else if (it.isJsonObject) {
|
||||||
|
val o = it.asJsonObject
|
||||||
|
val u = o.get("isian")?.asStringSafe()
|
||||||
|
?: o.get("url")?.asStringSafe()
|
||||||
|
?: o.get("path")?.asStringSafe()
|
||||||
|
?: ""
|
||||||
|
if (u.isNotBlank()) push(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
// dedup + mapping ke chip
|
||||||
|
return urls.distinct().map { u ->
|
||||||
|
SimpleAtt(url = u, fileName = fileNameFromUrl(u), label = "Lampiran")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAttachments(container: LinearLayout, msg: JsonObject?) {
|
||||||
|
container.removeAllViews()
|
||||||
|
|
||||||
|
val list = AttachmentExtractor.extractAll(msg)
|
||||||
|
|
||||||
|
// ✅ ambil lampiran dari aksi_komunikasi_lampiran (komunikasi detail jenis 11)
|
||||||
|
val aksiLampiran = extractAksiKomunikasiLampiran(msg)
|
||||||
|
|
||||||
|
val fallback = if (list.isEmpty() && aksiLampiran.isEmpty()) fallbackExtractAttachments(msg) else emptyList()
|
||||||
|
|
||||||
|
val merged = ArrayList<SimpleAtt>()
|
||||||
|
|
||||||
|
// list dari extractor utama (kalau ada)
|
||||||
|
for (att in list) {
|
||||||
|
merged.add(SimpleAtt(url = att.url, fileName = att.fileName, label = att.label))
|
||||||
|
}
|
||||||
|
|
||||||
|
// lampiran komunikasi detail
|
||||||
|
merged.addAll(aksiLampiran)
|
||||||
|
|
||||||
|
// fallback legacy
|
||||||
|
merged.addAll(fallback)
|
||||||
|
|
||||||
|
if (merged.isEmpty()) {
|
||||||
|
container.isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
container.isVisible = true
|
||||||
|
val inf = LayoutInflater.from(container.context)
|
||||||
|
|
||||||
|
for (att in merged) {
|
||||||
|
val chip = inf.inflate(R.layout.item_attachment_chip, container, false)
|
||||||
|
val tvName = chip.findViewById<TextView>(R.id.tv_name_attachment_chip)
|
||||||
|
val ibRemove = chip.findViewById<ImageButton>(R.id.ib_remove_attachment_chip)
|
||||||
|
ibRemove.isVisible = false
|
||||||
|
|
||||||
|
tvName.text = "${att.label} • ${att.fileName}"
|
||||||
|
chip.setOnClickListener {
|
||||||
|
openProtectedAttachment(container.context, att.url, att.fileName)
|
||||||
|
}
|
||||||
|
container.addView(chip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileNameFromUrl(url: String): String {
|
||||||
|
val clean = url.substringBefore("?").substringBefore("#")
|
||||||
|
return clean.substringAfterLast("/", clean)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openProtectedAttachment(ctx: Context, url: String, fileName: String) {
|
||||||
|
val rawToken = Preferences.getAccessToken(ctx).orEmpty()
|
||||||
|
if (rawToken.isBlank()) {
|
||||||
|
Toast.makeText(ctx, "Token kosong. Tidak bisa buka lampiran.", Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(ctx, "Membuka lampiran...", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
val fixedUrl = if (url.startsWith("http", true)) url else {
|
||||||
|
API_URL.trimEnd('/') + "/uploads/" + url.trimStart('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
val file = AttachmentDownloader.downloadToCache(
|
||||||
|
ctx = ctx,
|
||||||
|
url = fixedUrl,
|
||||||
|
fileName = fileName,
|
||||||
|
token = Preferences.getAccessToken(ctx)!!
|
||||||
|
)
|
||||||
|
|
||||||
|
(ctx as? Activity)?.runOnUiThread {
|
||||||
|
openDownloadedFile(ctx, file, fileName)
|
||||||
|
} ?: run {
|
||||||
|
openDownloadedFile(ctx, file, fileName, forceNewTask = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
(ctx as? Activity)?.runOnUiThread {
|
||||||
|
Toast.makeText(ctx, e.message ?: "Gagal buka lampiran", Toast.LENGTH_LONG).show()
|
||||||
|
} ?: run {
|
||||||
|
Toast.makeText(ctx, e.message ?: "Gagal buka lampiran", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDownloadedFile(ctx: Context, file: File, fileName: String, forceNewTask: Boolean = false) {
|
||||||
|
val isImage = runCatching { BitmapFactory.decodeFile(file.absolutePath) != null }.getOrDefault(false)
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
val i = Intent(ctx, AttachmentPreviewActivity::class.java).apply {
|
||||||
|
putExtra("local_path", file.absolutePath)
|
||||||
|
putExtra("title", fileName.ifBlank { file.name })
|
||||||
|
if (forceNewTask) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
ctx.startActivity(i)
|
||||||
|
} else {
|
||||||
|
openLocalFile(ctx, file, fileName, forceNewTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLocalFile(ctx: Context, file: File, fileName: String, forceNewTask: Boolean) {
|
||||||
|
val uri: Uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", file)
|
||||||
|
val mime = guessMime(fileName)
|
||||||
|
|
||||||
|
val i = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, mime)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
if (forceNewTask) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.startActivity(Intent.createChooser(i, "Buka lampiran"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun guessMime(fileName: String): String {
|
||||||
|
val ext = fileName.substringAfterLast('.', "").lowercase(Locale.getDefault())
|
||||||
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
||||||
|
return mime ?: "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// ✅ Komunikasi Detail (FIX)
|
||||||
|
// =========================
|
||||||
|
private data class DetailField(val label: String, val value: String)
|
||||||
|
|
||||||
|
private fun bindKomunikasiDetail(container: LinearLayout, msg: JsonObject?) {
|
||||||
|
container.removeAllViews()
|
||||||
|
if (msg == null) {
|
||||||
|
container.isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val details = extractKomunikasiDetailFields(msg)
|
||||||
|
if (details.isEmpty()) {
|
||||||
|
container.isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
container.isVisible = true
|
||||||
|
val inf = LayoutInflater.from(container.context)
|
||||||
|
|
||||||
|
for (d in details) {
|
||||||
|
val row = inf.inflate(R.layout.item_komunikasi_detail_row, container, false)
|
||||||
|
val tvLabel = row.findViewById<TextView>(R.id.tv_label)
|
||||||
|
val tvValue = row.findViewById<TextView>(R.id.tv_value)
|
||||||
|
|
||||||
|
tvLabel.text = d.label
|
||||||
|
tvValue.text = d.value.ifBlank { "-" }
|
||||||
|
|
||||||
|
container.addView(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractKomunikasiDetailFields(msg: JsonObject): List<DetailField> {
|
||||||
|
|
||||||
|
fun arrFrom(vararg paths: String): JsonArray? {
|
||||||
|
for (p in paths) {
|
||||||
|
var cur: JsonElement = msg
|
||||||
|
var ok = true
|
||||||
|
for (seg in p.split(".")) {
|
||||||
|
if (!cur.isJsonObject) { ok = false; break }
|
||||||
|
val o = cur.asJsonObject
|
||||||
|
if (!o.has(seg)) { ok = false; break }
|
||||||
|
cur = o.get(seg)
|
||||||
|
}
|
||||||
|
if (ok && cur.isJsonArray) return cur.asJsonArray
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JsonElement?.asStringSafeNullable(): String? {
|
||||||
|
if (this == null || this.isJsonNull) return null
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
this.isJsonPrimitive -> this.asString
|
||||||
|
this.isJsonArray -> this.asJsonArray.joinToString("\n") { it.asStringSafeNullable().orEmpty() }
|
||||||
|
this.isJsonObject -> {
|
||||||
|
val o = this.asJsonObject
|
||||||
|
val pick = listOf("nilai", "value", "teks", "string", "uraian", "isi")
|
||||||
|
.firstNotNullOfOrNull { k -> o.get(k)?.takeIf { !it.isJsonNull } }
|
||||||
|
pick?.asStringSafeNullable() ?: o.toString()
|
||||||
|
}
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun elementToListText(el: JsonElement?): String? {
|
||||||
|
if (el == null || el.isJsonNull) return null
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
el.isJsonArray -> el.asJsonArray.joinToString("\n") { "• " + it.asStringSafe() }
|
||||||
|
el.isJsonPrimitive -> el.asString
|
||||||
|
el.isJsonObject -> {
|
||||||
|
val o = el.asJsonObject
|
||||||
|
val v = o.get("nilai")
|
||||||
|
if (v != null && v.isJsonArray)
|
||||||
|
v.asJsonArray.joinToString("\n") { "• " + it.asStringSafe() }
|
||||||
|
else el.toString()
|
||||||
|
}
|
||||||
|
else -> el.toString()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readFromDetailValues(kodeDetail: Int, jenisId: Int): String? {
|
||||||
|
val dv = msg.get("detail_values")?.takeIf { it.isJsonObject }?.asJsonObject ?: return null
|
||||||
|
val el = dv.get(kodeDetail.toString()) ?: return null
|
||||||
|
return if (jenisId == JENIS_LIST) elementToListText(el)
|
||||||
|
else el.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readTypedSingle(jenisId: Int): String? {
|
||||||
|
return when (jenisId) {
|
||||||
|
|
||||||
|
JENIS_TANGGAL -> {
|
||||||
|
msg.get("aksi_komunikasi_tanggal")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_WAKTU -> {
|
||||||
|
msg.get("aksi_komunikasi_waktu")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_JAM -> {
|
||||||
|
msg.get("aksi_komunikasi_jam")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_ANGKA -> {
|
||||||
|
msg.get("aksi_komunikasi_angka")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_PECAHAN -> {
|
||||||
|
msg.get("aksi_komunikasi_pecahan")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_TEKS, JENIS_STRING -> {
|
||||||
|
msg.get("aksi_komunikasi_teks")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
?: msg.get("aksi_komunikasi_string")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("nilai")
|
||||||
|
.asStringSafeNullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_LIST -> {
|
||||||
|
val el = msg.get("aksi_komunikasi_list") ?: return null
|
||||||
|
extractListFromWeirdPayload(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
JENIS_LAMPIRAN -> {
|
||||||
|
val atts = extractAksiKomunikasiLampiran(msg)
|
||||||
|
if (atts.isEmpty()) return null
|
||||||
|
atts.joinToString("\n") { "• ${it.fileName}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val detailArr = arrFrom(
|
||||||
|
"komunikasi.komunikasi_detail",
|
||||||
|
"komunikasi_detail",
|
||||||
|
"aksi.komunikasi.komunikasi_detail"
|
||||||
|
) ?: return emptyList()
|
||||||
|
|
||||||
|
val out = ArrayList<DetailField>()
|
||||||
|
|
||||||
|
for (i in 0 until detailArr.size()) {
|
||||||
|
val d = detailArr[i]
|
||||||
|
if (!d.isJsonObject) continue
|
||||||
|
val dobj = d.asJsonObject
|
||||||
|
|
||||||
|
val label = dobj.get("isian").asStringSafe().ifBlank { "Field" }
|
||||||
|
|
||||||
|
val kodeDetail =
|
||||||
|
dobj.get("kode")?.asIntSafe()
|
||||||
|
?: dobj.get("kode_komunikasi_detail")?.asIntSafe()
|
||||||
|
?: continue
|
||||||
|
|
||||||
|
val jenisObj = dobj.get("jenis_isian")
|
||||||
|
?.takeIf { it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
|
||||||
|
val jenisId =
|
||||||
|
jenisObj?.get("id")?.asIntSafe()
|
||||||
|
?: dobj.get("id_jenis_isian")?.asIntSafe()
|
||||||
|
?: 0
|
||||||
|
|
||||||
|
val value =
|
||||||
|
readFromDetailValues(kodeDetail, jenisId)
|
||||||
|
?: readTypedSingle(jenisId)
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
if (value.isNotBlank()) {
|
||||||
|
out.add(DetailField(label, value.trim()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractListFromWeirdPayload(el: JsonElement): String? {
|
||||||
|
try {
|
||||||
|
// Case 1: langsung array
|
||||||
|
if (el.isJsonArray) {
|
||||||
|
val arr = el.asJsonArray
|
||||||
|
val values = mutableListOf<String>()
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
val it = arr[i]
|
||||||
|
when {
|
||||||
|
it.isJsonPrimitive -> values.add(it.asString)
|
||||||
|
it.isJsonObject -> {
|
||||||
|
val o = it.asJsonObject
|
||||||
|
val v = o.get("nilai")?.let { x ->
|
||||||
|
if (x.isJsonPrimitive) x.asString else x.toString()
|
||||||
|
}
|
||||||
|
if (!v.isNullOrBlank()) values.add(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values.takeIf { it.isNotEmpty() }?.joinToString("\n") { "• $it" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: object normal {nilai:[...]} atau {baris:[...], nilai:[...]}
|
||||||
|
if (el.isJsonObject) {
|
||||||
|
val o = el.asJsonObject
|
||||||
|
|
||||||
|
// 2a) standar: ada key "nilai"
|
||||||
|
val nilai = o.get("nilai")
|
||||||
|
if (nilai != null) {
|
||||||
|
return extractListFromWeirdPayload(nilai)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2b) payload kamu: key nya string dict -> value nya JsonArray
|
||||||
|
// ambil semua value yang JsonArray, gabung
|
||||||
|
val merged = mutableListOf<String>()
|
||||||
|
for ((_, v) in o.entrySet()) {
|
||||||
|
if (!v.isJsonArray) continue
|
||||||
|
val arr = v.asJsonArray
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
val it = arr[i]
|
||||||
|
if (it.isJsonNull) continue
|
||||||
|
val s = if (it.isJsonPrimitive) it.asString else it.toString()
|
||||||
|
if (s.isNotBlank()) merged.add(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged.takeIf { it.isNotEmpty() }?.joinToString("\n") { "• $it" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Helpers Json Safe
|
||||||
|
// =========================
|
||||||
|
private fun JsonElement.asStringSafe(): String {
|
||||||
|
return try {
|
||||||
|
if (isJsonNull) "" else if (isJsonPrimitive) asString else toString()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonElement.asIntSafe(): Int? {
|
||||||
|
return try {
|
||||||
|
if (isJsonNull) null else if (isJsonPrimitive) asInt else null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
app/src/main/java/com/amz/genie/adapters/GeneralAdapter.kt
Normal file
94
app/src/main/java/com/amz/genie/adapters/GeneralAdapter.kt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Utils.formatDateTime
|
||||||
|
import com.amz.genie.helpers.Utils.isFemale
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.OfficeTrnAksiKepada
|
||||||
|
import com.amz.genie.models.OfficeTrnReaksiKepada
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
class GeneralAdapter(
|
||||||
|
private val kodePegawai: String,
|
||||||
|
private val items: MutableList<GeneralThreadItem> = mutableListOf(),
|
||||||
|
private val onItemClick: ((GeneralThreadItem) -> Unit)? = null
|
||||||
|
) : RecyclerView.Adapter<GeneralAdapter.VH>() {
|
||||||
|
|
||||||
|
fun submitList(newItems: List<GeneralThreadItem>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val img: ShapeableImageView = itemView.findViewById(R.id.siv_employee_item_inbox)
|
||||||
|
private val tvName: TextView = itemView.findViewById(R.id.tv_employeename_item_inbox)
|
||||||
|
private val tvDateTime: TextView = itemView.findViewById(R.id.tv_datetime_item_inbox)
|
||||||
|
private val tvJobDesk: TextView = itemView.findViewById(R.id.tv_jobdesk_item_inbox)
|
||||||
|
private val tvDesc: TextView = itemView.findViewById(R.id.tv_description_item_inbox)
|
||||||
|
private val tvUnread: TextView = itemView.findViewById(R.id.tv_unread_item_inbox)
|
||||||
|
|
||||||
|
fun bind(row: GeneralThreadItem) {
|
||||||
|
val tipe = row.tipe.uppercase()
|
||||||
|
val aksi = row.aksi
|
||||||
|
val reaksi = row.reaksi
|
||||||
|
|
||||||
|
fun countUnreadAksi(kepada: List<OfficeTrnAksiKepada>?): Int {
|
||||||
|
return kepada?.count { it.is_aktif == 0 && it.kode_kepada == kodePegawai } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun countUnreadReaksi(kepada: List<OfficeTrnReaksiKepada>?): Int {
|
||||||
|
return kepada?.count { it.is_aktif == 0 && it.kode_kepada == kodePegawai } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tipe == "REAKSI" && reaksi != null) {
|
||||||
|
tvName.text = reaksi.pembuat.nama
|
||||||
|
tvDateTime.text = formatDateTime(reaksi.waktu_buat)
|
||||||
|
tvJobDesk.text = reaksi.pembuat.jabatan?.nama
|
||||||
|
tvDesc.text = reaksi.uraian?.takeIf { it.isNotBlank() } ?: "-"
|
||||||
|
|
||||||
|
val female = isFemale(reaksi.pembuat.id_kelamin)
|
||||||
|
img.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tvName.text = aksi.pembuat.nama
|
||||||
|
tvDateTime.text = formatDateTime(aksi.waktu_buat)
|
||||||
|
tvJobDesk.text = "${aksi.pembuat.jabatan?.nama} - ${aksi.pembuat.outlet?.nama}"
|
||||||
|
tvDesc.text = aksi.aksi_komunikasi_teks
|
||||||
|
?.nilai
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: aksi.uraian
|
||||||
|
|
||||||
|
val female = isFemale(aksi.pembuat.id_kelamin)
|
||||||
|
img.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalUnread = row.unread_count
|
||||||
|
if (totalUnread > 0) {
|
||||||
|
tvUnread.visibility = View.VISIBLE
|
||||||
|
tvUnread.text = totalUnread.toString()
|
||||||
|
} else {
|
||||||
|
tvUnread.visibility = View.GONE
|
||||||
|
tvUnread.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setOnClickListener { onItemClick?.invoke(row) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_inbox, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
}
|
||||||
171
app/src/main/java/com/amz/genie/adapters/GeneralDetailAdapter.kt
Normal file
171
app/src/main/java/com/amz/genie/adapters/GeneralDetailAdapter.kt
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Utils.formatDateTime
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.OfficeTrnAksiKepada
|
||||||
|
|
||||||
|
class GeneralDetailAdapter(
|
||||||
|
private val kodePegawai: String,
|
||||||
|
private val onItemClick: (GeneralThreadItem) -> Unit,
|
||||||
|
private val onDetailClick: (GeneralThreadItem) -> Unit
|
||||||
|
) : ListAdapter<GeneralThreadItem, GeneralDetailAdapter.UnifiedVH>(DIFF) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF = object : DiffUtil.ItemCallback<GeneralThreadItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: GeneralThreadItem, newItem: GeneralThreadItem): Boolean {
|
||||||
|
val oldKey = if (oldItem.tipe.equals("REAKSI", true))
|
||||||
|
"R_${oldItem.reaksi?.id ?: -1}"
|
||||||
|
else
|
||||||
|
"A_${oldItem.aksi.id}"
|
||||||
|
|
||||||
|
val newKey = if (newItem.tipe.equals("REAKSI", true))
|
||||||
|
"R_${newItem.reaksi?.id ?: -1}"
|
||||||
|
else
|
||||||
|
"A_${newItem.aksi.id}"
|
||||||
|
|
||||||
|
return oldKey == newKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: GeneralThreadItem, newItem: GeneralThreadItem): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitRawTimeline(raw: List<GeneralThreadItem>?) {
|
||||||
|
val dedup = raw.orEmpty()
|
||||||
|
.distinctBy {
|
||||||
|
if (it.tipe.equals("REAKSI", true)) "R_${it.reaksi?.id}" else "A_${it.aksi.id}"
|
||||||
|
}
|
||||||
|
submitList(dedup)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UnifiedVH {
|
||||||
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_inbox_detail, parent, false)
|
||||||
|
return UnifiedVH(v, kodePegawai, onItemClick, onDetailClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: UnifiedVH, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnifiedVH(
|
||||||
|
itemView: View,
|
||||||
|
private val kodePegawai: String,
|
||||||
|
private val onItemClick: (GeneralThreadItem) -> Unit,
|
||||||
|
private val onDetailClick: (GeneralThreadItem) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
private val tvTitle: TextView = itemView.findViewById(R.id.tv_title_item_inbox_detail)
|
||||||
|
private val tvTime: TextView = itemView.findViewById(R.id.tv_time_item_inbox_detail)
|
||||||
|
private val tvMessage: TextView = itemView.findViewById(R.id.tv_message_item_inbox_detail) // ✅ baru
|
||||||
|
|
||||||
|
private val rvPenerima: RecyclerView = itemView.findViewById(R.id.rv_penerima_item_inbox_detail)
|
||||||
|
private val rvSubject: RecyclerView = itemView.findViewById(R.id.rv_subject_item_inbox_detail)
|
||||||
|
private val llContainer: View = itemView.findViewById(R.id.ll_container_item_inbox_detail)
|
||||||
|
private val ibDetail: View = itemView.findViewById(R.id.ib_detail_item_inbox_detail)
|
||||||
|
|
||||||
|
private var currentItem: GeneralThreadItem? = null
|
||||||
|
|
||||||
|
private val penerimaAdapter = GeneralDetailPenerimaAdapter()
|
||||||
|
private val subjectAdapter = SubjectAdapter()
|
||||||
|
|
||||||
|
private val glm = GridLayoutManager(itemView.context, 3)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// ✅ 1 chip = full row, multi = 3 kolom
|
||||||
|
glm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
return if (penerimaAdapter.itemCount == 1) 3 else 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rvPenerima.layoutManager = glm
|
||||||
|
rvPenerima.adapter = penerimaAdapter
|
||||||
|
rvPenerima.isNestedScrollingEnabled = false
|
||||||
|
|
||||||
|
rvSubject.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
rvSubject.adapter = subjectAdapter
|
||||||
|
rvSubject.setHasFixedSize(true)
|
||||||
|
rvSubject.isNestedScrollingEnabled = false
|
||||||
|
|
||||||
|
ibDetail.setOnClickListener { currentItem?.let { onDetailClick(it) } }
|
||||||
|
itemView.setOnClickListener { currentItem?.let { onItemClick(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: GeneralThreadItem) {
|
||||||
|
currentItem = item
|
||||||
|
|
||||||
|
val isReaksi = item.tipe.equals("REAKSI", true)
|
||||||
|
val aksi = item.aksi
|
||||||
|
|
||||||
|
// ✅ WARNA berdasar is_aktif untuk saya
|
||||||
|
val inactiveForMe = aksi.kepada.orEmpty().any {
|
||||||
|
it.kode_kepada == kodePegawai && it.is_aktif == 0
|
||||||
|
}
|
||||||
|
llContainer.setBackgroundResource(
|
||||||
|
if (inactiveForMe) R.color.colorGreenVeryLight else R.color.colorWhitePure
|
||||||
|
)
|
||||||
|
|
||||||
|
// ✅ Title selalu judul/uraian AKSI
|
||||||
|
tvTitle.text = aksi.uraian
|
||||||
|
|
||||||
|
if (!isReaksi) {
|
||||||
|
// AKSI
|
||||||
|
tvTime.text = formatDateTime(aksi.waktu_buat)
|
||||||
|
|
||||||
|
penerimaAdapter.submitList(aksi.kepada.orEmpty())
|
||||||
|
|
||||||
|
val listString = arrayListOf(
|
||||||
|
aksi.tipe_komunikasi?.tipe_komunikasi.toString(),
|
||||||
|
aksi.tentang.tentang
|
||||||
|
)
|
||||||
|
subjectAdapter.submitList(listString)
|
||||||
|
|
||||||
|
// ✅ tidak ada pesan reaksi
|
||||||
|
tvMessage.visibility = View.GONE
|
||||||
|
tvMessage.text = ""
|
||||||
|
|
||||||
|
ibDetail.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// REAKSI
|
||||||
|
val rx = item.reaksi
|
||||||
|
|
||||||
|
tvTitle.text = aksi.uraian
|
||||||
|
tvTime.text = formatDateTime(rx?.waktu_buat ?: aksi.waktu_buat)
|
||||||
|
|
||||||
|
val pembuat = rx?.pembuat
|
||||||
|
val chip: List<OfficeTrnAksiKepada> = pembuat?.let { peg ->
|
||||||
|
listOf(
|
||||||
|
OfficeTrnAksiKepada(
|
||||||
|
id = -(rx?.id ?: 1),
|
||||||
|
aksi_id = aksi.id,
|
||||||
|
kode_kepada = peg.kode,
|
||||||
|
kepada = peg,
|
||||||
|
is_aktif = 1,
|
||||||
|
waktu_buat = rx?.waktu_buat ?: aksi.waktu_buat,
|
||||||
|
waktu_ubah = rx?.waktu_buat ?: aksi.waktu_buat
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
penerimaAdapter.submitList(chip)
|
||||||
|
subjectAdapter.submitList(arrayListOf("REAKSI"))
|
||||||
|
tvMessage.visibility = View.VISIBLE
|
||||||
|
tvMessage.text = rx?.uraian?.takeIf { it.isNotBlank() } ?: "(Reaksi)"
|
||||||
|
ibDetail.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.models.OfficeTrnAksiKepada
|
||||||
|
|
||||||
|
class GeneralDetailPenerimaAdapter :
|
||||||
|
ListAdapter<OfficeTrnAksiKepada, GeneralDetailPenerimaAdapter.VH>(DIFF) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF = object : DiffUtil.ItemCallback<OfficeTrnAksiKepada>() {
|
||||||
|
override fun areItemsTheSame(oldItem: OfficeTrnAksiKepada, newItem: OfficeTrnAksiKepada): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: OfficeTrnAksiKepada, newItem: OfficeTrnAksiKepada): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_inbox_detail_penerima, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tv: TextView = itemView.findViewById(R.id.tv_penerima_item)
|
||||||
|
|
||||||
|
fun bind(item: OfficeTrnAksiKepada) {
|
||||||
|
val nama = item.kepada.nama
|
||||||
|
tv.text = nama
|
||||||
|
tv.setBackgroundResource(R.drawable.bg_chip_penerima_dark_green)
|
||||||
|
tv.setTextColor(itemView.context.getColor(R.color.colorWhitePure))
|
||||||
|
|
||||||
|
val px = itemView.resources.getDimension(R.dimen.font_size_nano) // sudah px
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/src/main/java/com/amz/genie/adapters/RecipientAdapter.kt
Normal file
56
app/src/main/java/com/amz/genie/adapters/RecipientAdapter.kt
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Utils.isFemale
|
||||||
|
import com.amz.genie.models.Pegawai
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
class RecipientAdapter(
|
||||||
|
private val onRemove: (Pegawai) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecipientAdapter.VH>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<Pegawai>()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<Pegawai>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
val ivAvatar: ShapeableImageView = itemView.findViewById(R.id.iv_avatar_recipient)
|
||||||
|
val tvName: TextView = itemView.findViewById(R.id.tv_name_recipient)
|
||||||
|
val btnRemove: MaterialButton = itemView.findViewById(R.id.btn_remove_recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_recipient, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
holder.tvName.text = item.nama
|
||||||
|
|
||||||
|
// Avatar:
|
||||||
|
// Kalau Pegawai kamu punya field fotoUrl, nanti bisa pakai Glide/Coil.
|
||||||
|
// Untuk sekarang pakai default icon sudah cukup.
|
||||||
|
// holder.ivAvatar.setImageResource(R.drawable.ic_person_24)
|
||||||
|
|
||||||
|
val female = isFemale(item.id_kelamin)
|
||||||
|
holder.ivAvatar.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
|
||||||
|
holder.btnRemove.setOnClickListener {
|
||||||
|
onRemove(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.helpers.Utils.isFemale
|
||||||
|
import com.amz.genie.models.Pegawai
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
class RecipientPickerAdapter(
|
||||||
|
private val onPick: (Pegawai) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecipientPickerAdapter.VH>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<Pegawai>()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<Pegawai>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
val ivAvatar: ShapeableImageView = itemView.findViewById(R.id.iv_avatar_recipient)
|
||||||
|
val tvName: TextView = itemView.findViewById(R.id.tv_name_recipient)
|
||||||
|
val btnRemove: View = itemView.findViewById(R.id.btn_remove_recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_recipient, parent, false) // item XML yang kamu kasih
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
holder.tvName.text = item.nama
|
||||||
|
|
||||||
|
// di dialog, tombol remove tidak dipakai
|
||||||
|
holder.btnRemove.visibility = View.GONE
|
||||||
|
|
||||||
|
val female = isFemale(item.id_kelamin)
|
||||||
|
holder.ivAvatar.setImageResource(if (female) R.drawable.ic_woman else R.drawable.ic_man)
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener { onPick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
}
|
||||||
53
app/src/main/java/com/amz/genie/adapters/SubjectAdapter.kt
Normal file
53
app/src/main/java/com/amz/genie/adapters/SubjectAdapter.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package com.amz.genie.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.R
|
||||||
|
|
||||||
|
class SubjectAdapter(
|
||||||
|
private val items: ArrayList<String> = arrayListOf()
|
||||||
|
) : RecyclerView.Adapter<SubjectAdapter.VH>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_inbox_detail_penerima, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
holder.bind(items[position], position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tv: TextView = itemView.findViewById(R.id.tv_penerima_item)
|
||||||
|
|
||||||
|
fun bind(item: String, position: Int) {
|
||||||
|
tv.text = item
|
||||||
|
|
||||||
|
val isGreen = position % 2 == 0
|
||||||
|
|
||||||
|
tv.setBackgroundResource(
|
||||||
|
if (isGreen) R.drawable.bg_chip_penerima else R.drawable.bg_chip_penerima_gray
|
||||||
|
)
|
||||||
|
|
||||||
|
tv.setTextColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
itemView.context,
|
||||||
|
if (isGreen) R.color.colorBlack else R.color.colorWhitePure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitList(newItems: List<String>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
183
app/src/main/java/com/amz/genie/fragments/ArchiveFragment.kt
Normal file
183
app/src/main/java/com/amz/genie/fragments/ArchiveFragment.kt
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package com.amz.genie.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.GeneralDetailActivity
|
||||||
|
import com.amz.genie.activities.MainActivity
|
||||||
|
import com.amz.genie.adapters.GeneralAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.helpers.Utils.showEmpty
|
||||||
|
import com.amz.genie.models.GeneralResponse
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var srlArchive: SwipeRefreshLayout
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var rvArchive: RecyclerView
|
||||||
|
private lateinit var adapter: GeneralAdapter
|
||||||
|
private lateinit var detailLauncher: androidx.activity.result.ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
detailLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val needsRefresh = result.data?.getBooleanExtra("needs_refresh", false) ?: false
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK && needsRefresh) {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_archive, container, false)
|
||||||
|
initUI(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(view: View) {
|
||||||
|
srlArchive = view.findViewById(R.id.srl_archive)
|
||||||
|
tvEmpty = view.findViewById(R.id.tv_empty_archive)
|
||||||
|
rvArchive = view.findViewById(R.id.rl_archive)
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(requireContext()),
|
||||||
|
Pengguna::class.java)
|
||||||
|
adapter = GeneralAdapter(userData.pegawai?.kode!!) { item ->
|
||||||
|
val intent = Intent(requireContext(), GeneralDetailActivity::class.java)
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val dataJson = gson.toJson(item, GeneralThreadItem::class.java)
|
||||||
|
intent.putExtra("data", dataJson)
|
||||||
|
detailLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvArchive.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
rvArchive.adapter = adapter
|
||||||
|
rvArchive.setHasFixedSize(true)
|
||||||
|
|
||||||
|
srlArchive.setOnRefreshListener { initData() }
|
||||||
|
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
if (!isNetworkAvailable(requireContext())) {
|
||||||
|
showSnack(getString(R.string.no_internet_message))
|
||||||
|
srlArchive.isRefreshing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.list(
|
||||||
|
Preferences.getAccessToken(requireContext()),
|
||||||
|
4
|
||||||
|
)
|
||||||
|
.enqueue(object : Callback<GeneralResponse> {
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<GeneralResponse>, response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val list = response.body()?.items.orEmpty()
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
showEmpty(true, tvEmpty, rvArchive)
|
||||||
|
adapter.submitList(emptyList())
|
||||||
|
} else {
|
||||||
|
showEmpty(false, tvEmpty, rvArchive)
|
||||||
|
adapter.submitList(list)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<GeneralResponse>, t: Throwable) {
|
||||||
|
showLoading(false)
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(requireActivity())
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
srlArchive.isRefreshing = false
|
||||||
|
(activity as? MainActivity)?.showProgressDialog(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
(activity as? MainActivity)?.let { act ->
|
||||||
|
Snackbar.make(act.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
(activity as? MainActivity)?.setSearchClick({
|
||||||
|
Toast.makeText(requireContext(), "TEST RUN", Toast.LENGTH_LONG).show()
|
||||||
|
}, (activity as MainActivity).ibSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
183
app/src/main/java/com/amz/genie/fragments/AssignedFragment.kt
Normal file
183
app/src/main/java/com/amz/genie/fragments/AssignedFragment.kt
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package com.amz.genie.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.GeneralDetailActivity
|
||||||
|
import com.amz.genie.activities.MainActivity
|
||||||
|
import com.amz.genie.adapters.GeneralAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.helpers.Utils.showEmpty
|
||||||
|
import com.amz.genie.models.GeneralResponse
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
|
||||||
|
|
||||||
|
class AssignedFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var srlAssigned: SwipeRefreshLayout
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var rvAssigned: RecyclerView
|
||||||
|
private lateinit var adapter: GeneralAdapter
|
||||||
|
private lateinit var detailLauncher: androidx.activity.result.ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
detailLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val needsRefresh = result.data?.getBooleanExtra("needs_refresh", false) ?: false
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK && needsRefresh) {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_assigned, container, false)
|
||||||
|
initUI(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(view: View) {
|
||||||
|
srlAssigned = view.findViewById(R.id.srl_assigned)
|
||||||
|
tvEmpty = view.findViewById(R.id.tv_empty_assigned)
|
||||||
|
rvAssigned = view.findViewById(R.id.rl_assigned)
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(requireContext()),
|
||||||
|
Pengguna::class.java)
|
||||||
|
adapter = GeneralAdapter(userData.pegawai?.kode!!) { item ->
|
||||||
|
val intent = Intent(requireContext(), GeneralDetailActivity::class.java)
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val dataJson = gson.toJson(item, GeneralThreadItem::class.java)
|
||||||
|
intent.putExtra("data", dataJson)
|
||||||
|
detailLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvAssigned.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
rvAssigned.adapter = adapter
|
||||||
|
rvAssigned.setHasFixedSize(true)
|
||||||
|
|
||||||
|
srlAssigned.setOnRefreshListener { initData() }
|
||||||
|
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
if (!isNetworkAvailable(requireContext())) {
|
||||||
|
showSnack(getString(R.string.no_internet_message))
|
||||||
|
srlAssigned.isRefreshing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.list(
|
||||||
|
Preferences.getAccessToken(requireContext()),
|
||||||
|
3
|
||||||
|
)
|
||||||
|
.enqueue(object : Callback<GeneralResponse> {
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<GeneralResponse>, response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val list = response.body()?.items.orEmpty()
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
showEmpty(true, tvEmpty, rvAssigned)
|
||||||
|
adapter.submitList(emptyList())
|
||||||
|
} else {
|
||||||
|
showEmpty(false, tvEmpty, rvAssigned)
|
||||||
|
adapter.submitList(list)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<GeneralResponse>, t: Throwable) {
|
||||||
|
showLoading(false)
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(requireActivity())
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
srlAssigned.isRefreshing = false
|
||||||
|
(activity as? MainActivity)?.showProgressDialog(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
(activity as? MainActivity)?.let { act ->
|
||||||
|
Snackbar.make(act.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
(activity as? MainActivity)?.setSearchClick({
|
||||||
|
Toast.makeText(requireContext(), "TEST RUN", Toast.LENGTH_LONG).show()
|
||||||
|
}, (activity as MainActivity).ibSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.amz.genie.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.amz.genie.R
|
||||||
|
|
||||||
|
// TODO: Rename parameter arguments, choose names that match
|
||||||
|
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
|
||||||
|
private const val ARG_PARAM1 = "param1"
|
||||||
|
private const val ARG_PARAM2 = "param2"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* Use the [DashboardFragment.newInstance] factory method to
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class DashboardFragment : Fragment() {
|
||||||
|
// TODO: Rename and change types of parameters
|
||||||
|
private var param1: String? = null
|
||||||
|
private var param2: String? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
arguments?.let {
|
||||||
|
param1 = it.getString(ARG_PARAM1)
|
||||||
|
param2 = it.getString(ARG_PARAM2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.fragment_dashboard, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Use this factory method to create a new instance of
|
||||||
|
* this fragment using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param param1 Parameter 1.
|
||||||
|
* @param param2 Parameter 2.
|
||||||
|
* @return A new instance of fragment DashboardFragment.
|
||||||
|
*/
|
||||||
|
// TODO: Rename and change types and number of parameters
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(param1: String, param2: String) =
|
||||||
|
DashboardFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_PARAM1, param1)
|
||||||
|
putString(ARG_PARAM2, param2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
app/src/main/java/com/amz/genie/fragments/InboxFragment.kt
Normal file
180
app/src/main/java/com/amz/genie/fragments/InboxFragment.kt
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package com.amz.genie.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.GeneralDetailActivity
|
||||||
|
import com.amz.genie.activities.MainActivity
|
||||||
|
import com.amz.genie.adapters.GeneralAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.helpers.Utils.showEmpty
|
||||||
|
import com.amz.genie.models.GeneralResponse
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class InboxFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var srlInbox: SwipeRefreshLayout
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var rvInbox: RecyclerView
|
||||||
|
private lateinit var adapter: GeneralAdapter
|
||||||
|
private lateinit var detailLauncher: androidx.activity.result.ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
detailLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val needsRefresh = result.data?.getBooleanExtra("needs_refresh", false) ?: false
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK && needsRefresh) {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_inbox, container, false)
|
||||||
|
initUI(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(view: View) {
|
||||||
|
srlInbox = view.findViewById(R.id.srl_inbox)
|
||||||
|
tvEmpty = view.findViewById(R.id.tv_empty_inbox)
|
||||||
|
rvInbox = view.findViewById(R.id.rl_inbox)
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(requireContext()),
|
||||||
|
Pengguna::class.java)
|
||||||
|
adapter = GeneralAdapter(userData.pegawai?.kode!!) { item ->
|
||||||
|
val intent = Intent(requireContext(), GeneralDetailActivity::class.java)
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val dataJson = gson.toJson(item, GeneralThreadItem::class.java)
|
||||||
|
intent.putExtra("data", dataJson)
|
||||||
|
detailLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvInbox.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
rvInbox.adapter = adapter
|
||||||
|
rvInbox.setHasFixedSize(true)
|
||||||
|
|
||||||
|
srlInbox.setOnRefreshListener { initData() }
|
||||||
|
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
if (!isNetworkAvailable(requireContext())) {
|
||||||
|
showSnack(getString(R.string.no_internet_message))
|
||||||
|
srlInbox.isRefreshing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.list(
|
||||||
|
Preferences.getAccessToken(requireContext()),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
.enqueue(object : Callback<GeneralResponse> {
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<GeneralResponse>, response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val list = response.body()?.items.orEmpty()
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
showEmpty(true, tvEmpty, rvInbox)
|
||||||
|
adapter.submitList(emptyList())
|
||||||
|
} else {
|
||||||
|
showEmpty(false, tvEmpty, rvInbox)
|
||||||
|
adapter.submitList(list)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<GeneralResponse>, t: Throwable) {
|
||||||
|
showLoading(false)
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(requireActivity())
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
srlInbox.isRefreshing = false
|
||||||
|
(activity as? MainActivity)?.showProgressDialog(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
(activity as? MainActivity)?.let { act ->
|
||||||
|
Snackbar.make(act.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
(activity as? MainActivity)?.setSearchClick({
|
||||||
|
Toast.makeText(requireContext(), "TEST RUN", Toast.LENGTH_LONG).show()
|
||||||
|
}, (activity as MainActivity).ibSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
182
app/src/main/java/com/amz/genie/fragments/TODOFragment.kt
Normal file
182
app/src/main/java/com/amz/genie/fragments/TODOFragment.kt
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package com.amz.genie.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.GeneralDetailActivity
|
||||||
|
import com.amz.genie.activities.MainActivity
|
||||||
|
import com.amz.genie.adapters.GeneralAdapter
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
|
||||||
|
import com.amz.genie.helpers.Utils.isNetworkAvailable
|
||||||
|
import com.amz.genie.helpers.Utils.showEmpty
|
||||||
|
import com.amz.genie.models.GeneralResponse
|
||||||
|
import com.amz.genie.models.GeneralThreadItem
|
||||||
|
import com.amz.genie.models.Pengguna
|
||||||
|
import com.amz.genie.services.APIMain
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
|
||||||
|
|
||||||
|
class TODOFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var srlTODO: SwipeRefreshLayout
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var rvTODO: RecyclerView
|
||||||
|
private lateinit var adapter: GeneralAdapter
|
||||||
|
private lateinit var detailLauncher: androidx.activity.result.ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
detailLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val needsRefresh = result.data?.getBooleanExtra("needs_refresh", false) ?: false
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK && needsRefresh) {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_todo, container, false)
|
||||||
|
initUI(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUI(view: View) {
|
||||||
|
srlTODO = view.findViewById(R.id.srl_todo)
|
||||||
|
tvEmpty = view.findViewById(R.id.tv_empty_todo)
|
||||||
|
rvTODO = view.findViewById(R.id.rl_todo)
|
||||||
|
|
||||||
|
val userData = Gson().fromJson(Preferences.getUserData(requireContext()),
|
||||||
|
Pengguna::class.java)
|
||||||
|
adapter = GeneralAdapter(userData.pegawai?.kode!!) { item ->
|
||||||
|
val intent = Intent(requireContext(), GeneralDetailActivity::class.java)
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val dataJson = gson.toJson(item, GeneralThreadItem::class.java)
|
||||||
|
intent.putExtra("data", dataJson)
|
||||||
|
detailLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvTODO.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
rvTODO.adapter = adapter
|
||||||
|
rvTODO.setHasFixedSize(true)
|
||||||
|
|
||||||
|
srlTODO.setOnRefreshListener { initData() }
|
||||||
|
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
if (!isNetworkAvailable(requireContext())) {
|
||||||
|
showSnack(getString(R.string.no_internet_message))
|
||||||
|
srlTODO.isRefreshing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
APIMain.require().generalServices
|
||||||
|
.list(
|
||||||
|
Preferences.getAccessToken(requireContext()),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
.enqueue(object : Callback<GeneralResponse> {
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<GeneralResponse>, response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val list = response.body()?.items.orEmpty()
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
showEmpty(true, tvEmpty, rvTODO)
|
||||||
|
adapter.submitList(emptyList())
|
||||||
|
} else {
|
||||||
|
showEmpty(false, tvEmpty, rvTODO)
|
||||||
|
adapter.submitList(list)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<GeneralResponse>, t: Throwable) {
|
||||||
|
showLoading(false)
|
||||||
|
showSnack(t.message ?: "Terjadi kesalahan")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response<GeneralResponse>) {
|
||||||
|
showLoading(false)
|
||||||
|
val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
|
||||||
|
val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
(response.code() == 401 || response.code() == 500) && expired -> {
|
||||||
|
forceLogoutAndGoLogin(requireActivity())
|
||||||
|
"Session expired. Please login again."
|
||||||
|
}
|
||||||
|
response.code() == 400 -> runCatching {
|
||||||
|
JsonParser.parseString(raw).asJsonObject["message"].asString
|
||||||
|
}.getOrDefault("Bad request")
|
||||||
|
else -> "${response.code()}, ${response.message()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnack(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
srlTODO.isRefreshing = false
|
||||||
|
(activity as? MainActivity)?.showProgressDialog(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnack(message: String) {
|
||||||
|
(activity as? MainActivity)?.let { act ->
|
||||||
|
Snackbar.make(act.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
(activity as? MainActivity)?.setSearchClick({
|
||||||
|
Toast.makeText(requireContext(), "TEST RUN", Toast.LENGTH_LONG).show()
|
||||||
|
}, (activity as MainActivity).ibSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
setupActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
(activity as? MainActivity)?.ibSearch?.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object AttachmentDownloader {
|
||||||
|
|
||||||
|
fun downloadToCache(
|
||||||
|
ctx: Context,
|
||||||
|
url: String,
|
||||||
|
fileName: String,
|
||||||
|
token: String
|
||||||
|
): File {
|
||||||
|
val safeName = fileName.ifBlank { "attachment_${System.currentTimeMillis()}" }
|
||||||
|
val outFile = File(ctx.cacheDir, safeName)
|
||||||
|
|
||||||
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authorization", token)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(req).execute().use { resp ->
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
throw RuntimeException("Download gagal: ${resp.code} ${resp.message}")
|
||||||
|
}
|
||||||
|
val body = resp.body ?: throw RuntimeException("Body kosong")
|
||||||
|
outFile.outputStream().use { os ->
|
||||||
|
body.byteStream().use { input ->
|
||||||
|
input.copyTo(os)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outFile
|
||||||
|
}
|
||||||
|
}
|
||||||
137
app/src/main/java/com/amz/genie/helpers/AttachmentExtractor.kt
Normal file
137
app/src/main/java/com/amz/genie/helpers/AttachmentExtractor.kt
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class AttachmentItem(
|
||||||
|
val label: String, // "PDF", "JPG"
|
||||||
|
val fileName: String, // "xxx.jpg"
|
||||||
|
val relativePath: String, // "aksi/30030/xxx.jpg" / "aksi_lampiran/16/xxx.jpg" / "reaksi/xxx.jpg"
|
||||||
|
val url: String // full url
|
||||||
|
)
|
||||||
|
|
||||||
|
private enum class Source { AKSI, REAKSI, AKSI_LAMPIRAN, UNKNOWN }
|
||||||
|
|
||||||
|
object AttachmentExtractor {
|
||||||
|
fun extractAll(msg: JsonObject?): List<AttachmentItem> {
|
||||||
|
if (msg == null) return emptyList()
|
||||||
|
|
||||||
|
val base = Preferences.BASE_UPLOAD_URL.trimEnd('/') // contoh: https://api.../uploads
|
||||||
|
val out = mutableListOf<AttachmentItem>()
|
||||||
|
|
||||||
|
// A) lampiran umum (aksi) -> path_to_arsip: "aksi/30030/xxx.jpg"
|
||||||
|
out += extractFromArray(
|
||||||
|
arr = msg.getArray("lampiran"),
|
||||||
|
baseUrl = base,
|
||||||
|
defaultSource = Source.AKSI,
|
||||||
|
possiblePathKeys = listOf("path_to_arsip", "path", "isian", "file", "filename")
|
||||||
|
)
|
||||||
|
|
||||||
|
// B) lampiran reaksi -> kadang cuma filename "Screenshot_....jpg"
|
||||||
|
out += extractFromArray(
|
||||||
|
arr = msg.getArray("lampiran_reaksi"),
|
||||||
|
baseUrl = base,
|
||||||
|
defaultSource = Source.REAKSI,
|
||||||
|
possiblePathKeys = listOf("path_to_arsip", "path", "isian", "file", "filename")
|
||||||
|
)
|
||||||
|
|
||||||
|
// C) komunikasi detail lampiran -> isian/nilai/value: "aksi_lampiran/16/xxx.jpg"
|
||||||
|
out += extractFromArray(
|
||||||
|
arr = msg.getArray("aksi_komunikasi_lampiran"),
|
||||||
|
baseUrl = base,
|
||||||
|
defaultSource = Source.AKSI_LAMPIRAN,
|
||||||
|
possiblePathKeys = listOf("nilai", "value", "isian", "path", "path_to_arsip", "filename")
|
||||||
|
)
|
||||||
|
|
||||||
|
// D) fallback (optional)
|
||||||
|
val fallbackKeys = listOf("aksi_komunikasi_teks", "aksi_komunikasi_string")
|
||||||
|
for (k in fallbackKeys) {
|
||||||
|
out += extractFromArray(
|
||||||
|
arr = msg.getArray(k),
|
||||||
|
baseUrl = base,
|
||||||
|
defaultSource = Source.UNKNOWN,
|
||||||
|
possiblePathKeys = listOf("nilai", "value", "isian", "path", "path_to_arsip", "filename")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.distinctBy { it.url }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractFromArray(
|
||||||
|
arr: JsonArray?,
|
||||||
|
baseUrl: String,
|
||||||
|
defaultSource: Source,
|
||||||
|
possiblePathKeys: List<String>
|
||||||
|
): List<AttachmentItem> {
|
||||||
|
if (arr == null || arr.size() == 0) return emptyList()
|
||||||
|
|
||||||
|
val out = mutableListOf<AttachmentItem>()
|
||||||
|
|
||||||
|
for (el in arr) {
|
||||||
|
if (!el.isJsonObject) continue
|
||||||
|
val obj = el.asJsonObject
|
||||||
|
|
||||||
|
val rawPath = pickFirstString(obj, possiblePathKeys)?.trim().orEmpty()
|
||||||
|
if (rawPath.isBlank()) continue
|
||||||
|
|
||||||
|
val normalized = normalizeRelativePath(rawPath, defaultSource) ?: continue
|
||||||
|
|
||||||
|
val fileName = normalized.substringAfterLast('/')
|
||||||
|
if (fileName.isBlank()) continue
|
||||||
|
|
||||||
|
val ext = fileName.substringAfterLast('.', missingDelimiterValue = "")
|
||||||
|
.lowercase(Locale.getDefault())
|
||||||
|
|
||||||
|
val label = if (ext.isBlank()) "FILE" else ext.uppercase(Locale.getDefault())
|
||||||
|
val url = "$baseUrl/$normalized"
|
||||||
|
|
||||||
|
out += AttachmentItem(
|
||||||
|
label = label,
|
||||||
|
fileName = fileName,
|
||||||
|
relativePath = normalized,
|
||||||
|
url = url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickFirstString(obj: JsonObject, keys: List<String>): String? {
|
||||||
|
for (k in keys) {
|
||||||
|
val el = obj.get(k) ?: continue
|
||||||
|
if (el.isJsonNull) continue
|
||||||
|
val s = runCatching { el.asString }.getOrNull()
|
||||||
|
if (!s.isNullOrBlank()) return s
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sesuai data DB kamu:
|
||||||
|
* - aksi: "aksi/30030/...."
|
||||||
|
* - aksi_lampiran: "aksi_lampiran/16/...."
|
||||||
|
* - reaksi: kadang cuma filename -> jadi "reaksi/<filename>"
|
||||||
|
*/
|
||||||
|
private fun normalizeRelativePath(raw: String, defaultSource: Source): String? {
|
||||||
|
val p = raw.replace("\\", "/").trim().trimStart('/')
|
||||||
|
if (p.isBlank()) return null
|
||||||
|
|
||||||
|
val lower = p.lowercase(Locale.getDefault())
|
||||||
|
if (lower.startsWith("aksi/") || lower.startsWith("aksi_lampiran/") || lower.startsWith("reaksi/")) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// cuma filename
|
||||||
|
return when (defaultSource) {
|
||||||
|
Source.REAKSI -> "reaksi/$p"
|
||||||
|
Source.AKSI -> "aksi/$p"
|
||||||
|
Source.AKSI_LAMPIRAN -> "aksi_lampiran/$p"
|
||||||
|
Source.UNKNOWN -> p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extension helper
|
||||||
|
private fun JsonObject.getArray(key: String) =
|
||||||
|
get(key)?.takeIf { it.isJsonArray }?.asJsonArray
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
|
||||||
|
object ChatMessageRenderer {
|
||||||
|
fun render(message: JsonObject?): String {
|
||||||
|
if (message == null) return ""
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
// 1) uraian (kalau ada)
|
||||||
|
val uraian = message.getString("uraian")
|
||||||
|
if (!uraian.isNullOrBlank()) {
|
||||||
|
sb.append(uraian.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) komunikasi / tipe_komunikasi / tentang (optional)
|
||||||
|
// message.getObj("komunikasi")?.getString("komunikasi")?.let { addLine(sb, "Komunikasi", it) }
|
||||||
|
// message.getObj("tipe_komunikasi")?.getString("tipe_komunikasi")?.let { addLine(sb, "Tipe", it) }
|
||||||
|
// message.getObj("tentang")?.getString("tentang")?.let { addLine(sb, "Tentang", it) }
|
||||||
|
|
||||||
|
// 3) Render field komunikasi dinamis (list, angka, teks, tanggal, waktu, jam, pecahan, string)
|
||||||
|
// Semua ini umumnya array of object yang punya "isian" dan "nilai" (atau variasinya).
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_teks", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_string", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_angka", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_pecahan", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_tanggal", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_waktu", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_jam", message)
|
||||||
|
addSectionFromArray(sb, "aksi_komunikasi_list", message)
|
||||||
|
|
||||||
|
// 4) Lampiran (jika ingin tampil sebagai teks)
|
||||||
|
val lampiranArr = message.getArray("lampiran")
|
||||||
|
if (lampiranArr != null && lampiranArr.size() > 0) {
|
||||||
|
// tampilkan ringkas saja
|
||||||
|
addLine(sb, "Lampiran", "${lampiranArr.size()} file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 5) Lampiran REAKSI (taruh di sini)
|
||||||
|
val lampReaksi = message.getArray("lampiran_reaksi")
|
||||||
|
if (lampReaksi != null && lampReaksi.size() > 0) {
|
||||||
|
addLine(sb, "Lampiran", "${lampReaksi.size()} file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kalau kosong total, fallback tampilkan JSON singkat
|
||||||
|
return sb.toString().ifBlank { message.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSectionFromArray(sb: StringBuilder, key: String, msg: JsonObject) {
|
||||||
|
val arr = msg.getArray(key) ?: return
|
||||||
|
if (arr.size() == 0) return
|
||||||
|
|
||||||
|
for (el in arr) {
|
||||||
|
if (!el.isJsonObject) continue
|
||||||
|
val obj = el.asJsonObject
|
||||||
|
|
||||||
|
// Label pertanyaan/isian
|
||||||
|
val label = obj.getString("isian")
|
||||||
|
?: obj.getString("label")
|
||||||
|
?: obj.getString("nama")
|
||||||
|
?: key
|
||||||
|
|
||||||
|
// Nilai jawaban (banyak variasi field di backend)
|
||||||
|
val value = obj.getString("nilai")
|
||||||
|
?: obj.getString("value")
|
||||||
|
?: obj.getString("uraian")
|
||||||
|
?: obj.getString("isi")
|
||||||
|
?: obj.getString("jawaban")
|
||||||
|
|
||||||
|
if (!value.isNullOrBlank()) {
|
||||||
|
addLine(sb, label, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLine(sb: StringBuilder, label: String, value: String) {
|
||||||
|
if (sb.isNotEmpty()) sb.append("\n")
|
||||||
|
sb.append("• ").append(label).append(": ").append(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Json helpers ----
|
||||||
|
private fun JsonObject.getString(key: String): String? {
|
||||||
|
val el = get(key) ?: return null
|
||||||
|
if (el.isJsonNull) return null
|
||||||
|
return runCatching { el.asString }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.getObj(key: String): JsonObject? {
|
||||||
|
val el = get(key) ?: return null
|
||||||
|
if (el.isJsonNull || !el.isJsonObject) return null
|
||||||
|
return el.asJsonObject
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.getArray(key: String) =
|
||||||
|
get(key)?.takeIf { it.isJsonArray }?.asJsonArray
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import androidx.emoji2.emojipicker.EmojiPickerView
|
||||||
|
|
||||||
|
class EmojiPickerBottomSheet(
|
||||||
|
private val onEmojiPicked: (String) -> Unit
|
||||||
|
) : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val picker = EmojiPickerView(requireContext())
|
||||||
|
|
||||||
|
picker.setOnEmojiPickedListener { emojiViewItem ->
|
||||||
|
onEmojiPicked(emojiViewItem.emoji)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/src/main/java/com/amz/genie/helpers/Preferences.kt
Normal file
61
app/src/main/java/com/amz/genie/helpers/Preferences.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
object Preferences {
|
||||||
|
const val PREFS_NAME = "genieData"
|
||||||
|
// const val API_URL = "https://api-genie.naminaniar.com/api/"
|
||||||
|
// const val API_URL = "http://192.168.21.102:5000/api/"
|
||||||
|
// const val API_URL = "http://192.160.3.17:5000/api/"
|
||||||
|
// const val API_URL = "http://192.168.18.19:5000/api/"
|
||||||
|
const val API_URL = "http://192.168.1.22:5000/api/"
|
||||||
|
const val BASE_UPLOAD_URL = "https://api-genie.naminaniar.com/uploads"
|
||||||
|
const val PROFILE_PICTURE_URL = "https://api-genie.naminaniar.com/profile_picture"
|
||||||
|
private const val USER = "user"
|
||||||
|
private const val REFRESH_TOKEN = "refreshToken"
|
||||||
|
private const val ACCESS_TOKEN = "accessToken"
|
||||||
|
private const val KEY_LAST_FCM_TOPIC = "last_fcm_topic"
|
||||||
|
|
||||||
|
fun getLastFcmTopic(ctx: Context): String {
|
||||||
|
val sp = ctx.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
return sp.getString(KEY_LAST_FCM_TOPIC, "") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastFcmTopic(ctx: Context, topic: String) {
|
||||||
|
val sp = ctx.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
sp.edit { putString(KEY_LAST_FCM_TOPIC, topic) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun setRefreshToken(context: Context, value: String?) {
|
||||||
|
preferences(context).edit { putString(REFRESH_TOKEN, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAccessToken(context: Context, value: String?) {
|
||||||
|
preferences(context).edit { putString(ACCESS_TOKEN, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAccessToken(context: Context): String? =
|
||||||
|
preferences(context).getString(ACCESS_TOKEN, null)
|
||||||
|
|
||||||
|
fun getRefreshToken(context: Context): String? =
|
||||||
|
preferences(context)
|
||||||
|
.getString(REFRESH_TOKEN, null)
|
||||||
|
|
||||||
|
fun setUserData(context: Context, value: String?) {
|
||||||
|
preferences(context).edit { putString(USER, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserData(context: Context): String? =
|
||||||
|
preferences(context).getString(USER, null)
|
||||||
|
|
||||||
|
fun clearAll(context: Context) {
|
||||||
|
preferences(context).edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/java/com/amz/genie/helpers/SimpleTextWatcher.kt
Normal file
12
app/src/main/java/com/amz/genie/helpers/SimpleTextWatcher.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
|
||||||
|
class SimpleTextWatcher(private val onChanged: (String) -> Unit) : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
onChanged(s?.toString().orEmpty())
|
||||||
|
}
|
||||||
|
override fun afterTextChanged(s: Editable?) {}
|
||||||
|
}
|
||||||
221
app/src/main/java/com/amz/genie/helpers/Utils.kt
Normal file
221
app/src/main/java/com/amz/genie/helpers/Utils.kt
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package com.amz.genie.helpers
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.amz.genie.activities.LoginActivity
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object Utils {
|
||||||
|
fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatDateTime(raw: String): String {
|
||||||
|
return try {
|
||||||
|
val locale = Locale("id", "ID")
|
||||||
|
val inFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", locale)
|
||||||
|
val date = inFmt.parse(raw) ?: return raw
|
||||||
|
|
||||||
|
fun isSameDay(a: Calendar, b: Calendar): Boolean {
|
||||||
|
return a.get(Calendar.YEAR) == b.get(Calendar.YEAR) &&
|
||||||
|
a.get(Calendar.DAY_OF_YEAR) == b.get(Calendar.DAY_OF_YEAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cal = Calendar.getInstance().apply { time = date }
|
||||||
|
val today = Calendar.getInstance()
|
||||||
|
val yesterday = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -1) }
|
||||||
|
|
||||||
|
val timeStr = SimpleDateFormat("HH:mm", locale).format(date)
|
||||||
|
|
||||||
|
when {
|
||||||
|
isSameDay(cal, today) -> timeStr
|
||||||
|
isSameDay(cal, yesterday) -> "Kemarin $timeStr"
|
||||||
|
isWithinLastDays(date, 7) -> {
|
||||||
|
val dayStr = SimpleDateFormat("EEEE", locale).format(date).replace(".", "")
|
||||||
|
"$dayStr $timeStr"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val dateStr = SimpleDateFormat("dd/MM/yy", locale).format(date)
|
||||||
|
"$dateStr $timeStr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWithinLastDays(date: Date, days: Int): Boolean {
|
||||||
|
val diff = System.currentTimeMillis() - date.time
|
||||||
|
val limit = days.toLong() * 24L * 60L * 60L * 1000L
|
||||||
|
return diff in 0 until limit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFemale(gender: String?): Boolean {
|
||||||
|
return gender == "P"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showEmpty(isEmpty: Boolean, tvEmpty: TextView, rl: RecyclerView) {
|
||||||
|
tvEmpty.visibility = if (isEmpty) View.VISIBLE else View.GONE
|
||||||
|
rl.visibility = if (isEmpty) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.queryDisplayName(uri: Uri): String? {
|
||||||
|
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||||
|
return runCatching {
|
||||||
|
contentResolver.query(uri, projection, null, null, null)?.use { cursor: Cursor ->
|
||||||
|
val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (idx == -1) return@use null
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getString(idx)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.copyUriToCacheFile(
|
||||||
|
uri: Uri,
|
||||||
|
prefix: String = "ATTACH_"
|
||||||
|
): File {
|
||||||
|
val mime = contentResolver.getType(uri).orEmpty()
|
||||||
|
|
||||||
|
val nameFromProvider = queryDisplayName(uri)
|
||||||
|
val extFromMime = runCatching {
|
||||||
|
MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||||
|
}.getOrNull().orEmpty()
|
||||||
|
|
||||||
|
// Nama file aman
|
||||||
|
val baseName = nameFromProvider?.takeIf { it.isNotBlank() }
|
||||||
|
?: "${prefix}${System.currentTimeMillis()}"
|
||||||
|
|
||||||
|
val safeName = when {
|
||||||
|
extFromMime.isNotBlank() && !baseName.endsWith(".$extFromMime", true) -> "$baseName.$extFromMime"
|
||||||
|
else -> baseName
|
||||||
|
}
|
||||||
|
|
||||||
|
val outFile = File(cacheDir, safeName)
|
||||||
|
|
||||||
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
FileOutputStream(outFile).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
} ?: throw IllegalStateException("Gagal membuka file dari Uri: $uri")
|
||||||
|
|
||||||
|
return outFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.uriToMultipartPart(
|
||||||
|
partName: String,
|
||||||
|
uri: Uri,
|
||||||
|
filenameOverride: String? = null
|
||||||
|
): MultipartBody.Part {
|
||||||
|
val mime = contentResolver.getType(uri).orEmpty()
|
||||||
|
val file = copyUriToCacheFile(uri)
|
||||||
|
|
||||||
|
val mediaType = mime.toMediaTypeOrNull()
|
||||||
|
val body = file.asRequestBody(mediaType)
|
||||||
|
|
||||||
|
val filename = filenameOverride ?: (queryDisplayName(uri) ?: file.name)
|
||||||
|
return MultipartBody.Part.createFormData(partName, filename, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forceLogoutAndGoLogin(activity: Activity) {
|
||||||
|
val firebaseReady = FirebaseApp.getApps(activity.applicationContext).isNotEmpty()
|
||||||
|
|
||||||
|
// ambil topic sebelum clear prefs
|
||||||
|
val oldTopic = Preferences.getLastFcmTopic(activity).trim()
|
||||||
|
|
||||||
|
fun finishLogout() {
|
||||||
|
// clear semua data setelah unsubscribe selesai
|
||||||
|
Preferences.clearAll(activity)
|
||||||
|
goLoginClearTask(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign out FirebaseAuth (kalau dipakai)
|
||||||
|
runCatching {
|
||||||
|
if (firebaseReady) FirebaseAuth.getInstance().signOut()
|
||||||
|
}.onFailure {
|
||||||
|
Log.w("Logout", "FirebaseAuth.signOut failed: ${it.message}", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firebaseReady) {
|
||||||
|
finishLogout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Unsubscribe dari topic terakhir (kalau ada)
|
||||||
|
val doDeleteToken = {
|
||||||
|
runCatching {
|
||||||
|
FirebaseMessaging.getInstance().deleteToken()
|
||||||
|
.addOnCompleteListener {
|
||||||
|
finishLogout()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Log.w("Logout", "deleteToken failed: ${it.message}", it)
|
||||||
|
finishLogout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTopic.isNotBlank()) {
|
||||||
|
FirebaseMessaging.getInstance().unsubscribeFromTopic(oldTopic)
|
||||||
|
.addOnCompleteListener { task ->
|
||||||
|
Log.d(
|
||||||
|
"Logout",
|
||||||
|
"unsubscribe topic=$oldTopic success=${task.isSuccessful} err=${task.exception}"
|
||||||
|
)
|
||||||
|
// bersihkan last topic
|
||||||
|
Preferences.setLastFcmTopic(activity, "")
|
||||||
|
doDeleteToken()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doDeleteToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun goLoginClearTask(activity: Activity) {
|
||||||
|
val intent = Intent(activity, LoginActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
activity.startActivity(intent)
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uriToBase64(context: Context, uri: Uri): String? {
|
||||||
|
return try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
val bytes = inputStream?.readBytes()
|
||||||
|
inputStream?.close()
|
||||||
|
if (bytes != null) {
|
||||||
|
Base64.encodeToString(bytes, Base64.DEFAULT)
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
5
app/src/main/java/com/amz/genie/models/ActivityItem.kt
Normal file
5
app/src/main/java/com/amz/genie/models/ActivityItem.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class ActivityItem(
|
||||||
|
val text: String
|
||||||
|
)
|
||||||
9
app/src/main/java/com/amz/genie/models/AddActionItem.kt
Normal file
9
app/src/main/java/com/amz/genie/models/AddActionItem.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class AddActionItem(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val idTentang: String? = null,
|
||||||
|
val idKomunikasi: String? = null,
|
||||||
|
val komunikasi_detail: ArrayList<KomunikasiDetail>? = null,
|
||||||
|
)
|
||||||
24
app/src/main/java/com/amz/genie/models/Aksi.kt
Normal file
24
app/src/main/java/com/amz/genie/models/Aksi.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Aksi(
|
||||||
|
val id: Int,
|
||||||
|
val komunikasi: Komunikasi,
|
||||||
|
val tentang: Tentang,
|
||||||
|
val tipe_komunikasi: TipeKomunikasi? = null,
|
||||||
|
val pembuat: Pegawai,
|
||||||
|
val uraian: String,
|
||||||
|
val selesai: String = "T",
|
||||||
|
val waktu_buat: String,
|
||||||
|
val kepada: ArrayList<OfficeTrnAksiKepada>? = null,
|
||||||
|
val lampiran: ArrayList<OfficeTrnAksiLampiran>? = null,
|
||||||
|
val reaksi: ArrayList<Reaksi>? = null,
|
||||||
|
val aksi_komunikasi_angka: OfficeTrnAksiKomunikasiAngka? = null,
|
||||||
|
val aksi_komunikasi_jam: OfficeTrnAksiKomunikasiJam? = null,
|
||||||
|
val aksi_komunikasi_lampiran: ArrayList<OfficeTrnAksiKomunikasiLampiran>? = null,
|
||||||
|
val aksi_komunikasi_list: ArrayList<OfficeTrnAksiKomunikasiList>? = null,
|
||||||
|
val aksi_komunikasi_pecahan: OfficeTrnAksiKomunikasiPecahan? = null,
|
||||||
|
val aksi_komunikasi_string: OfficeTrnAksiKomunikasiString? = null,
|
||||||
|
val aksi_komunikasi_tanggal: OfficeTrnAksiKomunikasiTanggal? = null,
|
||||||
|
val aksi_komunikasi_teks: OfficeTrnAksiKomunikasiTeks? = null,
|
||||||
|
val aksi_komunikasi_waktu: OfficeTrnAksiKomunikasiWaktu? = null,
|
||||||
|
)
|
||||||
5
app/src/main/java/com/amz/genie/models/AttachmentItem.kt
Normal file
5
app/src/main/java/com/amz/genie/models/AttachmentItem.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class AttachmentItem(val uri: Uri, val name: String, val mime: String)
|
||||||
16
app/src/main/java/com/amz/genie/models/ChatItem.kt
Normal file
16
app/src/main/java/com/amz/genie/models/ChatItem.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
|
||||||
|
data class ChatItem(
|
||||||
|
val id: String,
|
||||||
|
val senderKode: String?,
|
||||||
|
val senderName: String?,
|
||||||
|
val senderJob: String?,
|
||||||
|
val senderOutlet: String?,
|
||||||
|
val message: JsonObject?,
|
||||||
|
val timeText: String?,
|
||||||
|
val isMine: Boolean,
|
||||||
|
val isSameSenderAsPrev: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
7
app/src/main/java/com/amz/genie/models/FormAttachment.kt
Normal file
7
app/src/main/java/com/amz/genie/models/FormAttachment.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class FormAttachment(
|
||||||
|
val uri: android.net.Uri,
|
||||||
|
val fileName: String,
|
||||||
|
val mimeType: String
|
||||||
|
)
|
||||||
61
app/src/main/java/com/amz/genie/models/GeneralResponse.kt
Normal file
61
app/src/main/java/com/amz/genie/models/GeneralResponse.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
|
||||||
|
data class GeneralResponse(
|
||||||
|
val items: ArrayList<GeneralThreadItem>? = arrayListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GeneralThreadItem(
|
||||||
|
val counterpart: String? = null,
|
||||||
|
val tipe: String, // "AKSI" atau "REAKSI"
|
||||||
|
val aksi: Aksi,
|
||||||
|
val unread_count: Int = 0,
|
||||||
|
val reaksi: Reaksi? = null,
|
||||||
|
val is_unread: Boolean? = null,
|
||||||
|
val is_aktif: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GeneralDetailResponse(
|
||||||
|
val counterpart: Pegawai? = null,
|
||||||
|
val items: List<GeneralThreadItem> = emptyList(),
|
||||||
|
val meta: Meta? = null
|
||||||
|
) {
|
||||||
|
data class Meta(
|
||||||
|
val page: Int? = null,
|
||||||
|
val per_page: Int? = null,
|
||||||
|
val total: Int? = null,
|
||||||
|
val has_more: Boolean? = null,
|
||||||
|
val next_page: Int? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InboxThreadResponse(
|
||||||
|
val items: List<RawMessage>?,
|
||||||
|
val meta: Meta?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Meta(
|
||||||
|
val page: Int?,
|
||||||
|
val per_page: Int?,
|
||||||
|
val total: Int?,
|
||||||
|
val has_more: Boolean?,
|
||||||
|
val next_page: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sender(
|
||||||
|
val nama: String?,
|
||||||
|
val jabatan: Jabatan?,
|
||||||
|
val outlet: Outlet?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RawMessage(
|
||||||
|
val id: String?,
|
||||||
|
val sender_kode: String,
|
||||||
|
val sender: Sender?,
|
||||||
|
val message: JsonObject?,
|
||||||
|
val waktu_buat: String?,
|
||||||
|
val tipe: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
6
app/src/main/java/com/amz/genie/models/Jabatan.kt
Normal file
6
app/src/main/java/com/amz/genie/models/Jabatan.kt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Jabatan(
|
||||||
|
val kode: Int,
|
||||||
|
val nama: String
|
||||||
|
)
|
||||||
10
app/src/main/java/com/amz/genie/models/Komunikasi.kt
Normal file
10
app/src/main/java/com/amz/genie/models/Komunikasi.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Komunikasi(
|
||||||
|
val kode: Int,
|
||||||
|
val id_tipe_komunikasi: String,
|
||||||
|
val id_tentang: String,
|
||||||
|
val komunikasi: String,
|
||||||
|
val id_status_data: Int,
|
||||||
|
val komunikasi_detail: ArrayList<KomunikasiDetail>?
|
||||||
|
)
|
||||||
24
app/src/main/java/com/amz/genie/models/KomunikasiDetail.kt
Normal file
24
app/src/main/java/com/amz/genie/models/KomunikasiDetail.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class KomunikasiDetail(
|
||||||
|
val kode: Int? = null,
|
||||||
|
val kode_komunikasi: Int,
|
||||||
|
val urutan: Int? = null,
|
||||||
|
val id_jenis_isian: Int,
|
||||||
|
val jenis_isian: OfficeRefJenisIsian? = null,
|
||||||
|
val isian: String,
|
||||||
|
val petunjuk: String? = null,
|
||||||
|
val is_wajib: Int = 1,
|
||||||
|
val is_aktif: Int = 1,
|
||||||
|
val nilai_pemicu: String?,
|
||||||
|
val is_wajib_kondisional: Int = 0,
|
||||||
|
val id_tahap_isian: String = "A",
|
||||||
|
val operator_pemicu: String?,
|
||||||
|
val kode_detail_induk: Int? = null,
|
||||||
|
val is_separator: Int = 0,
|
||||||
|
val is_pertanyaan: Int = 0,
|
||||||
|
val is_rantai_komunikasi: Int = 0,
|
||||||
|
val is_pemicu_tindak_lanjut_isian: Int = 0,
|
||||||
|
val max_item: Int,
|
||||||
|
val min_item: Int
|
||||||
|
)
|
||||||
7
app/src/main/java/com/amz/genie/models/Login.kt
Normal file
7
app/src/main/java/com/amz/genie/models/Login.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Login(
|
||||||
|
val access_token: String,
|
||||||
|
val refresh_token: String,
|
||||||
|
val user: Pengguna
|
||||||
|
)
|
||||||
5
app/src/main/java/com/amz/genie/models/Message.kt
Normal file
5
app/src/main/java/com/amz/genie/models/Message.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Message(
|
||||||
|
val message: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeRefJenisIsian(
|
||||||
|
val id: Int,
|
||||||
|
val nama_jenis_isian: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKepada(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val kode_kepada: String,
|
||||||
|
val kepada: Pegawai,
|
||||||
|
val id_status_komunikasi: String = "T",
|
||||||
|
val id_kotak_pesan: Int = 1,
|
||||||
|
val is_adhoc: Int = 0,
|
||||||
|
val is_aktif: Int = 0,
|
||||||
|
val is_selesai: Int = 0,
|
||||||
|
val id_status_data: Int = 1,
|
||||||
|
val waktu_buat: String,
|
||||||
|
val waktu_ubah: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiAngka(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiJam(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiLampiran(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiList(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val baris: Int,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiPecahan(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: Float,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiString(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiTanggal(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiTeks(
|
||||||
|
val id: Int? = null,
|
||||||
|
val aksi_id: Int? = null,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiKomunikasiWaktu(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: Int,
|
||||||
|
val komunikasi_detail: KomunikasiDetail? = null,
|
||||||
|
val nilai: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnAksiLampiran(
|
||||||
|
val id: Int,
|
||||||
|
val aksi_id: String,
|
||||||
|
val path_to_arsip: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnReaksiKepada(
|
||||||
|
val id: Int,
|
||||||
|
val reaksi_id: Int,
|
||||||
|
val kode_kepada: String,
|
||||||
|
val id_status_komunikasi: String = "T",
|
||||||
|
val id_kotak_pesan: Int = 1,
|
||||||
|
val is_adhoc: Int = 0,
|
||||||
|
val is_aktif: Int = 0,
|
||||||
|
val is_selesai: Int = 0,
|
||||||
|
val waktu_buat: String,
|
||||||
|
val waktu_ubah: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class OfficeTrnReaksiLampiran(
|
||||||
|
val id: Int,
|
||||||
|
val reaksi_id: String,
|
||||||
|
val path_to_arsip: String,
|
||||||
|
)
|
||||||
7
app/src/main/java/com/amz/genie/models/Outlet.kt
Normal file
7
app/src/main/java/com/amz/genie/models/Outlet.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Outlet(
|
||||||
|
val kode: Int,
|
||||||
|
val nama: String,
|
||||||
|
val singkatan: String,
|
||||||
|
)
|
||||||
11
app/src/main/java/com/amz/genie/models/Pegawai.kt
Normal file
11
app/src/main/java/com/amz/genie/models/Pegawai.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Pegawai(
|
||||||
|
val kode: String,
|
||||||
|
val nama: String,
|
||||||
|
val outlet: Outlet?,
|
||||||
|
val jabatan: Jabatan?,
|
||||||
|
val mulai_bekerja: String,
|
||||||
|
val id_kelamin: String,
|
||||||
|
val outlets: ArrayList<Outlet>?
|
||||||
|
)
|
||||||
9
app/src/main/java/com/amz/genie/models/Pengguna.kt
Normal file
9
app/src/main/java/com/amz/genie/models/Pengguna.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Pengguna(
|
||||||
|
val kode: Int,
|
||||||
|
val pegawai: Pegawai?,
|
||||||
|
val email: String?,
|
||||||
|
val telephone: String?,
|
||||||
|
val username: String
|
||||||
|
)
|
||||||
10
app/src/main/java/com/amz/genie/models/PostAksi.kt
Normal file
10
app/src/main/java/com/amz/genie/models/PostAksi.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class PostAksi(
|
||||||
|
var kode: Int?,
|
||||||
|
var kepada: List<String>,
|
||||||
|
var tentang: String,
|
||||||
|
var topic: String,
|
||||||
|
var uraian: String,
|
||||||
|
var komunikasiDetail: ArrayList<KomunikasiDetail>?
|
||||||
|
)
|
||||||
16
app/src/main/java/com/amz/genie/models/Reaksi.kt
Normal file
16
app/src/main/java/com/amz/genie/models/Reaksi.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Reaksi(
|
||||||
|
val id: Int,
|
||||||
|
val id_aksi: Int? = null,
|
||||||
|
val id_reaksi: Int? = null,
|
||||||
|
val id_jenis_reaksi: String? = null,
|
||||||
|
val kode_komunikasi: Int? = null,
|
||||||
|
val tentang: Tentang? = null,
|
||||||
|
val tipe_komunikasi: TipeKomunikasi? = null,
|
||||||
|
val pembuat: Pegawai,
|
||||||
|
val uraian: String?,
|
||||||
|
val waktu_buat: String,
|
||||||
|
val kepada_reaksi: ArrayList<OfficeTrnReaksiKepada>? = null,
|
||||||
|
val lampiran_reaksi: ArrayList<OfficeTrnReaksiLampiran>? = null
|
||||||
|
)
|
||||||
10
app/src/main/java/com/amz/genie/models/ReaksiPost.kt
Normal file
10
app/src/main/java/com/amz/genie/models/ReaksiPost.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class ReaksiPost(
|
||||||
|
var aksi_id: Int,
|
||||||
|
var jenis_reaksi: String,
|
||||||
|
var reaksi_id: Int? = null,
|
||||||
|
var tentang: String,
|
||||||
|
var topic: String,
|
||||||
|
var uraian: String
|
||||||
|
)
|
||||||
7
app/src/main/java/com/amz/genie/models/ReaksiResponse.kt
Normal file
7
app/src/main/java/com/amz/genie/models/ReaksiResponse.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class ReaksiResponse(
|
||||||
|
val message: String,
|
||||||
|
val reaksi_id: Int,
|
||||||
|
val thread: InboxThreadResponse
|
||||||
|
)
|
||||||
11
app/src/main/java/com/amz/genie/models/Tentang.kt
Normal file
11
app/src/main/java/com/amz/genie/models/Tentang.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class Tentang(
|
||||||
|
val id: String,
|
||||||
|
val tentang: String,
|
||||||
|
val keterangan: String?,
|
||||||
|
val is_aktif: Int = 1,
|
||||||
|
val id_status_data: Int = 1,
|
||||||
|
val waktu_ubah: String,
|
||||||
|
val diubah_oleh: Int? = null
|
||||||
|
)
|
||||||
11
app/src/main/java/com/amz/genie/models/TipeKomunikasi.kt
Normal file
11
app/src/main/java/com/amz/genie/models/TipeKomunikasi.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.amz.genie.models
|
||||||
|
|
||||||
|
data class TipeKomunikasi(
|
||||||
|
val id: String,
|
||||||
|
val tipe_komunikasi: String,
|
||||||
|
val keterangan: String?,
|
||||||
|
val is_aktif: Int = 1,
|
||||||
|
val id_status_data: Int = 1,
|
||||||
|
val waktu_ubah: String,
|
||||||
|
val diubah_oleh: Int? = null
|
||||||
|
)
|
||||||
100
app/src/main/java/com/amz/genie/services/APIMain.kt
Normal file
100
app/src/main/java/com/amz/genie/services/APIMain.kt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import com.amz.genie.helpers.Preferences
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class APIMain: Application() {
|
||||||
|
companion object {
|
||||||
|
lateinit var instance: APIMain
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun require(): APIMain {
|
||||||
|
check(::instance.isInitialized) {
|
||||||
|
"APIMain belum diinisialisasi. Pastikan android:name='.services.APIMain' sudah ada " +
|
||||||
|
"di AndroidManifest dan jangan panggil APIMain() secara langsung."
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lateinit var accountServices: AuthServices
|
||||||
|
lateinit var generalServices: GeneralServices
|
||||||
|
lateinit var reaksiServices: ReaksiServices
|
||||||
|
lateinit var selectionServices: SelectionServices
|
||||||
|
lateinit var actionServices: ActionServices
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
// Paksa aplikasi selalu LIGHT (disable dark mode)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
val firebaseApp = runCatching { FirebaseApp.initializeApp(this) }.getOrNull()
|
||||||
|
if (firebaseApp == null) {
|
||||||
|
Log.e("FirebaseInit", "FirebaseApp.initializeApp() returned NULL. App lanjut tanpa Firebase.")
|
||||||
|
} else {
|
||||||
|
runCatching {
|
||||||
|
FirebaseMessaging.getInstance().isAutoInitEnabled = true
|
||||||
|
Log.d("FirebaseInit", "Firebase ready, FCM autoInit enabled")
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("FirebaseInit", "FCM init failed: ${it.message}", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("APIMain", "onCreate() called, initializing Retrofit...")
|
||||||
|
|
||||||
|
val tokenRepo = TokenRepository(this)
|
||||||
|
|
||||||
|
val isDebug = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
|
val logging = HttpLoggingInterceptor().apply {
|
||||||
|
level = if (isDebug) HttpLoggingInterceptor.Level.BODY
|
||||||
|
else HttpLoggingInterceptor.Level.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(logging)
|
||||||
|
.connectTimeout(100, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(3600, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.callTimeout(3600, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val retrofitAuth = Retrofit.Builder()
|
||||||
|
.baseUrl(Preferences.API_URL)
|
||||||
|
.client(baseClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
val authServices = retrofitAuth.create(AuthServices::class.java)
|
||||||
|
|
||||||
|
val mainClient = baseClient.newBuilder()
|
||||||
|
.addInterceptor(HeaderInterceptor(tokenRepo))
|
||||||
|
.addInterceptor(ExpiredRetryInterceptor(tokenRepo, authServices))
|
||||||
|
.authenticator(TokenAuthenticator(tokenRepo, authServices))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(Preferences.API_URL)
|
||||||
|
.client(mainClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
accountServices = retrofit.create(AuthServices::class.java)
|
||||||
|
generalServices = retrofit.create(GeneralServices::class.java)
|
||||||
|
reaksiServices = retrofit.create(ReaksiServices::class.java)
|
||||||
|
selectionServices = retrofit.create(SelectionServices::class.java)
|
||||||
|
actionServices = retrofit.create(ActionServices::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/src/main/java/com/amz/genie/services/ActionServices.kt
Normal file
20
app/src/main/java/com/amz/genie/services/ActionServices.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
|
|
||||||
|
interface ActionServices {
|
||||||
|
@Multipart
|
||||||
|
@POST("aksi/add")
|
||||||
|
fun add(
|
||||||
|
@Header("Authorization") token: String?,
|
||||||
|
@Part("data") data: RequestBody,
|
||||||
|
@Part files: List<MultipartBody.Part>? = emptyList()
|
||||||
|
): Call<Message>
|
||||||
|
}
|
||||||
34
app/src/main/java/com/amz/genie/services/AuthServices.kt
Normal file
34
app/src/main/java/com/amz/genie/services/AuthServices.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import com.amz.genie.models.Login
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface AuthServices {
|
||||||
|
@POST("auth/refresh")
|
||||||
|
fun refresh(
|
||||||
|
@Header("Authorization") refreshAuthorization: String // "Bearer <refresh_token>"
|
||||||
|
): Call<RefreshResponse>
|
||||||
|
|
||||||
|
@POST("login")
|
||||||
|
fun login(
|
||||||
|
@Body requestBody: RequestBody
|
||||||
|
): Call<Login>
|
||||||
|
|
||||||
|
@DELETE("api/{path}")
|
||||||
|
fun logoutDynamic(
|
||||||
|
@Path("path") path: String,
|
||||||
|
@Header("Authorization") token: String?
|
||||||
|
): Call<Message>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RefreshResponse(
|
||||||
|
@SerializedName("access_token") val accessToken: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class ExpiredRetryInterceptor(private val tokenRepo: TokenRepository,
|
||||||
|
private val authServices: AuthServices
|
||||||
|
) : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
var req = chain.request()
|
||||||
|
var resp = chain.proceed(req)
|
||||||
|
|
||||||
|
if (resp.code == 500) {
|
||||||
|
val peek = resp.peekBody(1024 * 1024).string()
|
||||||
|
val looksExpired = peek.contains("token_expired", true) ||
|
||||||
|
peek.contains("Signature has expired", true)
|
||||||
|
|
||||||
|
if (looksExpired) {
|
||||||
|
resp.close()
|
||||||
|
val refresh = tokenRepo.getRefreshToken()
|
||||||
|
if (!refresh.isNullOrBlank()) {
|
||||||
|
val r = try { authServices.refresh("Bearer $refresh").execute() } catch (_: Exception) { null }
|
||||||
|
val newAccess = r?.body()?.accessToken
|
||||||
|
if (r != null && r.isSuccessful && !newAccess.isNullOrBlank()) {
|
||||||
|
tokenRepo.saveAccessToken(newAccess)
|
||||||
|
req = req.newBuilder()
|
||||||
|
.header("Authorization", "Bearer $newAccess")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(req) // retry sekali
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/src/main/java/com/amz/genie/services/GeneralServices.kt
Normal file
44
app/src/main/java/com/amz/genie/services/GeneralServices.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import com.amz.genie.models.GeneralDetailResponse
|
||||||
|
import com.amz.genie.models.GeneralResponse
|
||||||
|
import com.amz.genie.models.InboxThreadResponse
|
||||||
|
import com.amz.genie.models.Message
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface GeneralServices {
|
||||||
|
@GET("general/list")
|
||||||
|
fun list(
|
||||||
|
@Header("Authorization") token: String?,
|
||||||
|
@Query("general_id") generalId: Int
|
||||||
|
): Call<GeneralResponse>
|
||||||
|
|
||||||
|
@GET("inbox/detail")
|
||||||
|
fun detail(
|
||||||
|
@Header("Authorization") token: String?,
|
||||||
|
@Query("counterpart_kode") counterPartKode: String?,
|
||||||
|
@Query("page") page: Int,
|
||||||
|
@Query("per_page") perPage: Int
|
||||||
|
): Call<GeneralDetailResponse>
|
||||||
|
|
||||||
|
@GET("inbox/thread")
|
||||||
|
fun threadDetail(
|
||||||
|
@Header("Authorization") token: String?,
|
||||||
|
@Query("counterpart_kode") counterpart: String,
|
||||||
|
@Query("aksi_id") aksiId: Int,
|
||||||
|
@Query("page") page: Int,
|
||||||
|
@Query("per_page") perPage: Int
|
||||||
|
): Call<InboxThreadResponse>
|
||||||
|
|
||||||
|
@POST("inbox/readed")
|
||||||
|
fun readed(
|
||||||
|
@Header("Authorization") token: String?,
|
||||||
|
@Query("aksi_reaksi_id") aksiReaksiId: Int,
|
||||||
|
@Query("tipe") tipe: String,
|
||||||
|
): Call<Message>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class HeaderInterceptor(
|
||||||
|
private val tokenRepo: TokenRepository
|
||||||
|
) : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val original = chain.request()
|
||||||
|
val b = original.newBuilder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
|
||||||
|
val access = tokenRepo.getAccessToken()
|
||||||
|
if (!access.isNullOrBlank()) {
|
||||||
|
b.header("Authorization", access)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(b.method(original.method, original.body).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.amz.genie.services
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.amz.genie.R
|
||||||
|
import com.amz.genie.activities.MainActivity
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import androidx.core.app.TaskStackBuilder
|
||||||
|
import com.amz.genie.activities.GeneralDetailActivity
|
||||||
|
import com.amz.genie.activities.GeneralSubDetailActivity
|
||||||
|
|
||||||
|
|
||||||
|
class MyFirebaseMessagingService: FirebaseMessagingService() {
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
super.onNewToken(token)
|
||||||
|
Log.d("FCM", "Refreshed token: $token")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
|
super.onMessageReceived(remoteMessage)
|
||||||
|
|
||||||
|
val titleFromNotif = remoteMessage.notification?.title
|
||||||
|
val bodyFromNotif = remoteMessage.notification?.body
|
||||||
|
|
||||||
|
val title = remoteMessage.data["title"] ?: titleFromNotif
|
||||||
|
val body = remoteMessage.data["body"] ?: bodyFromNotif
|
||||||
|
|
||||||
|
Log.d("FCM", "data=${remoteMessage.data} notifTitle=$titleFromNotif")
|
||||||
|
|
||||||
|
if (!title.isNullOrBlank() || !body.isNullOrBlank() || remoteMessage.data.isNotEmpty()) {
|
||||||
|
sendNotification(title, body, remoteMessage.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureChannels(notificationManager: NotificationManager) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
val soundUri = "android.resource://${packageName}/${R.raw.notification}".toUri()
|
||||||
|
val attrs = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// DEFAULT
|
||||||
|
val defaultChannel = NotificationChannel(
|
||||||
|
"genie_default_v1",
|
||||||
|
"Genie - Umum",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
// URGENT (lebih kenceng)
|
||||||
|
val urgentChannel = NotificationChannel(
|
||||||
|
"genie_urgent_v1",
|
||||||
|
"Genie - Penting",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
enableVibration(true)
|
||||||
|
setSound(soundUri, attrs)
|
||||||
|
// enableLights(true) // optional
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(defaultChannel)
|
||||||
|
notificationManager.createNotificationChannel(urgentChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildClickPendingIntent(data: Map<String, String>, requestCode: Int): PendingIntent {
|
||||||
|
val open = data["open"] ?: "main" // main | inbox_detail | inbox_subdetail
|
||||||
|
val dataJson = data["data"] // JSON InboxThreadItem
|
||||||
|
val counterpart = data["counterpart"] // untuk subdetail
|
||||||
|
|
||||||
|
// 1) MainActivity dulu (biar masuk app dan stack rapi)
|
||||||
|
val mainIntent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
// optional: kasih sinyal supaya Main fokus ke inbox tab
|
||||||
|
putExtra("open_tab", "inbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
val stack = TaskStackBuilder.create(this)
|
||||||
|
stack.addNextIntent(mainIntent)
|
||||||
|
|
||||||
|
val aksiId = data["aksi_id"] ?: data["id_aksi"] // support dua key
|
||||||
|
val cp = data["counterpart"] ?: data["kode_pembuat"]
|
||||||
|
val tentangId = data["tentang_id"] ?: data["id_tentang"]
|
||||||
|
|
||||||
|
if (open == "inbox_subdetail") {
|
||||||
|
// Kalau ada JSON lama, tetap support
|
||||||
|
if (!dataJson.isNullOrBlank()) {
|
||||||
|
val detailIntent = Intent(this, GeneralDetailActivity::class.java).apply {
|
||||||
|
putExtra("data", dataJson)
|
||||||
|
}
|
||||||
|
stack.addNextIntent(detailIntent)
|
||||||
|
|
||||||
|
val subIntent = Intent(this, GeneralSubDetailActivity::class.java).apply {
|
||||||
|
putExtra("data", dataJson)
|
||||||
|
if (!counterpart.isNullOrBlank()) putExtra("counterpart", counterpart)
|
||||||
|
}
|
||||||
|
stack.addNextIntent(subIntent)
|
||||||
|
} else if (!aksiId.isNullOrBlank() && !cp.isNullOrBlank()) {
|
||||||
|
// ✅ Mode baru: buka thread pakai cp + aksi_id (tanpa JSON)
|
||||||
|
val subIntent = Intent(this, GeneralSubDetailActivity::class.java).apply {
|
||||||
|
putExtra("aksi_id", aksiId)
|
||||||
|
putExtra("counterpart_kode", cp)
|
||||||
|
if (!tentangId.isNullOrBlank()) putExtra("tentang_id", tentangId)
|
||||||
|
// optional: putExtra("tipe", data["tipe"])
|
||||||
|
}
|
||||||
|
stack.addNextIntent(subIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.getPendingIntent(
|
||||||
|
requestCode,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendNotification(
|
||||||
|
title: String?,
|
||||||
|
message: String?,
|
||||||
|
data: Map<String, String>
|
||||||
|
) {
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
ensureChannels(notificationManager)
|
||||||
|
|
||||||
|
val isUrgent = data["is_urgent"] == "yes"
|
||||||
|
val channelId = if (isUrgent) "genie_urgent_v1" else "genie_default_v1"
|
||||||
|
|
||||||
|
val soundUri = "android.resource://${packageName}/${R.raw.notification}".toUri()
|
||||||
|
|
||||||
|
// Intent ketika notif diklik
|
||||||
|
val notificationId = (System.currentTimeMillis() % Int.MAX_VALUE).toInt()
|
||||||
|
val pendingIntent = buildClickPendingIntent(data, notificationId)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(R.drawable.logo_normal)
|
||||||
|
.setContentTitle(title ?: "Genie")
|
||||||
|
.setContentText(message ?: "Ada notifikasi masuk")
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(message ?: "Ada notifikasi masuk"))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent) // ✅ INI KUNCI: biar klik ada aksi
|
||||||
|
.setPriority(
|
||||||
|
if (isUrgent) NotificationCompat.PRIORITY_HIGH
|
||||||
|
else NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
// Android < 8 (Oreo) perlu set sound langsung
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
builder.setSound(soundUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(notificationId, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user