Stepper Status Tiket yang Jujur: Belajar dari Array History
Stepper tiket nggak nebak lagi dari satu string status: waktunya diambil dari riwayat transisi, satu baris pertama per status.
Ringkasan
Awalnya stepper tiket cuma nebak progres dari satu string status jadi sering ngaco. Sekarang progress diambil dari array history pakai Map buat nyimpen timestamp pertama tiap status, dengan empat tahap aja dan fallback kalau data kosong. Form pilar juga dibenerin pakai as const biar TypeScript ngejaga inputnya tetep valid.
Pas saya buka ticket-flow.tsx di project KotaPortal, saya cuma bisa geleng kepala. Komponen stepper yang harusnya nunjukin progres tiket malah isinya tebakan. Kita cuma punya satu string status dari API, terus kode frontend dipaksa nurunin tiga boolean dari string itu. Kalo statusnya diproses, stepper nebak sendiri ini tahap dua atau tiga. Waktu transisinya? Cuma ada di level agregat, nggak ada detail per tahap.
Salah kaprah soal jumlah step
Awalnya saya mikir, solusinya ya bikin stepper dengan enam step karena ada enam kemungkinan status di database. Saya juga sempat ngira kita harus minta tim backend nambahin field timestamp baru buat tiap tahap. Rasanya masuk akal sih, secara visual kita butuh enam titik waktu. Saya bahkan udah nyusun skema baru di kepala.
Tapi pas saya cek lagi kontrak OpenAPI, ternyata endpoint-nya udah ngirim array history. Asumsi enam step langsung jebol. User nggak butuh enam lingkaran kecil yang bikin pusing di layar. Empat tahap utama aja udah cukup, dan tahap terakhir menampung tiga status terminal: disetujui, ditolak, atau selesai.
Mengambil waktu dari array history
Daripada nebak, lebih baik timeline digerakkan dari riwayat transisi yang asli. Di ticket-flow.tsx, saya ubah tipe TicketHistoryItem langsung dari kontrak yang di-generate, skema StatusHistoryItem [6]. Tipe dari generator openapi-typescript itu runtime-free: musnah pas compile, jadi nggak nambah ukuran bundle [6].
Logikanya pendek. Sebuah Map nyimpen timestamp: tiap row history dicek to_status-nya, dan kalau status itu belum ada di map, timestamp-nya masuk. Row pertama yang mencatat suatu status itulah waktu transisinya. Kalo array history kosong atau gagal keambil, kode fallback ke waktu buat dan waktu terakhir diubah dari data utama tiket.
Saya sempat kena masalah pas nyoba parse tanggal dari history. String tanggal yang formatnya nggak standar itu implementasinya beda-beda tiap browser, dan string yang invalid di-parse jadi NaN [5]. Fungsi pembentuk tanggal di komponen ini jadi selalu ngecek hasil parse sebelum dipakai; NaN artinya jangan tampilin apa-apa, bukan tampilin teks aneh.
Memisahkan tampilan dan data
Pemisahan empat tahap tampilan versus enam status data ini ngebantu banget. Fungsi flowStageOf cuma milih tahap mana yang aktif, sementara isFinalStatus ngecek apakah status sekarang udah masuk kelompok terminal.
Buat nampilin riwayat lengkapnya di bawah stepper, saya pakai tag ol [7], karena daftar ini urutannya bermakna, kayak langkah resep atau arah jalan [7]. Atribut data-testid saya pasang biar gampang dites automation. Kalo ada string status aneh yang nggak dikenal, fungsi labelOf nge-fallback ke teks mentahnya biar UI nggak blank.
Transformasi data buat rendering ini saya taruh di top level component, bukan di dalam useEffect. React nyaranin transformasi data jalan pas render langsung dari props, biar nggak ada render bolak-balik yang nggak perlu [4]. Kalau kamu coba pola ini dan muncul warning soal state update di dalam effect, itu tandanya logika transformasinya masih nanggung di tempat yang salah.
Validasi form pakai literal types
Commit yang sama beresin form pendaftaran. Dulu user bisa submit pilar yang nggak ada di daftar. Sekarang daftar opsinya didefinisikan as const, terus tipenya ditarik dari array itu sendiri jadi union string literal [1]. Di compile time, TypeScript langsung komplain kalau ada nilai di luar union. Di runtime, guard-nya tetap validasi form: pilar wajib dipilih sebelum submit. Jadi tipe bikin salah ketahuan pas nulis kode, bukan pas user mengeluh.
Saya pribadi lebih suka pendekatan defensif kayak gini. Daripada nambahin validasi manual yang rawan lupa, mending biarin compiler yang jaga bagian compile, dan form yang jaga bagian runtime. Stepper yang baik itu nggak nebak kondisi; dia nunjukin fakta dari history yang tercatat, dan input yang masuk udah dibikin mustahil salah sejak form.