first commit
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user