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() private val selectedRecipients = mutableListOf() // value form dinamis: Int / Double / String / MutableList / MutableList private val formValues = linkedMapOf() // untuk validasi & setError field angka/pecahan/text/tanggal/waktu/jam/string private val fieldTilByKode = linkedMapOf() private val fieldEtByKode = linkedMapOf() // untuk list (error wajib list) private val listInputEtByKode = linkedMapOf() // untuk lampiran (tampilan list nama file + error text) private val lampiranTvFilesByKode = linkedMapOf() private val lampiranTvErrorByKode = linkedMapOf() 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) ?: 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(R.id.ll_add_template_action).removeAllViews() } initData() setupActions() } // ========================= // Dynamic Form Renderer // ========================= private fun renderDynamicForm(details: List) { val container = findViewById(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(R.id.til) val et = v.findViewById(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(R.id.til) val et = v.findViewById(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(R.id.til) val et = v.findViewById(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(R.id.til) val et = v.findViewById(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(R.id.til) val et = v.findViewById(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(R.id.tv_label) val etItem = v.findViewById(R.id.et_item) val btnAdd = v.findViewById(R.id.btn_add) val tvItems = v.findViewById(R.id.tv_items) tvLabel.text = if (wajib) "$label *" else label val items = mutableListOf() 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(R.id.tv_label) val btnPick = v.findViewById(R.id.btn_pick) val btnClear = v.findViewById(R.id.btn_clear) val tvFiles = v.findViewById(R.id.tv_files) val tvError = v.findViewById(R.id.tv_error) tvLabel.text = if (wajib) "$label *" else label val list = mutableListOf() 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(R.id.til) val et = v.findViewById(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(R.id.til) val et = v.findViewById(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).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): 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): List> { 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).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, komunikasiDetail: List> ): 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 { val parts = mutableListOf() formValues.forEach { (kodeDetail, v) -> val atts = (v as? List<*>)?.filterIsInstance().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> { override fun onResponse( call: Call>, response: Response> ) { 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>, 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> { override fun onResponse( call: Call>, response: Response> ) { 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>, 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 { override fun onResponse(call: Call, response: Response) { 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, 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) { 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(R.id.rv_recipient_picker) val sv = v.findViewById(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() } }