989 lines
37 KiB
Kotlin
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()
|
|
}
|
|
} |