Files
genie-android/app/src/main/java/com/amz/genie/activities/AddTemplateActionActivity.kt
2026-03-03 01:25:13 +07:00

989 lines
37 KiB
Kotlin

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