API Aplikasi Karyawan

Acuan lengkap bagi pengembang aplikasi seluler CompanyBrain. Seluruh bentuk data di halaman ini dibaca langsung dari kode backend yang sedang berjalan, bukan disusun dari ingatan.

Versi API v1 Diperbarui 25 Agustus 2026

Halaman ini menjelaskan 26 endpoint yang dipakai aplikasi karyawan hari ini. Permukaan yang sebenarnya jauh lebih luas: 211 endpoint dapat dipanggil seorang karyawan, mulai dari email perusahaan, persetujuan sebagai atasan, sampai seluruh operasi lapangan.

Daftar lengkapnya ada di Acuan endpoint lengkap, dibangkitkan langsung dari kode backend. Halaman yang sedang Anda baca tetap menjadi tempat menjelaskan konsep dan jebakannya; yang di sana adalah daftar.

Yang perlu dibaca lebih dahulu. Tiga hal di bawah ini menentukan hampir seluruh keputusan rancangan aplikasi, dan salah memahaminya membuat aplikasi tampak berfungsi lalu gagal di lapangan:

Perusahaan ditentukan dari alamat yang dihubungi, bukan dari isi permintaan. Balasan 403 adalah jawaban yang sah, bukan selalu galat. Enam ketidakcocokan yang sudah diketahui antara aplikasi yang ada sekarang dan backend.

Mulai dari sini

Alur terpendek dari layar kosong sampai daftar kehadiran:

# 1. Ambil identitas perusahaan (tanpa token, menentukan nama dan warna layar masuk)
curl https://os.perusahaan-contoh.id/api/v1/public/branding

# 2. Masuk dengan kode karyawan dan PIN
curl -X POST https://os.perusahaan-contoh.id/api/v1/auth/employee-login \
  -H 'Content-Type: application/json' \
  -d '{"employee_code":"EMP-0010","pin":"735291"}'

# 3. Pakai access_token pada seluruh permintaan berikutnya
curl https://os.perusahaan-contoh.id/api/v1/attendance?from=2026-08-01&to=2026-08-31 \
  -H 'Authorization: Bearer eyJ0eXAiOi...'

Alamat dan perusahaan

Akar seluruh alamat adalah https://<host-perusahaan>/api/v1.

Perusahaan ditentukan backend dari header Host. Tidak ada parameter tenant, tidak ada header tambahan, dan tidak ada pengenal perusahaan di dalam badan permintaan. Yang menentukan "aplikasi ini milik siapa" hanyalah alamat yang dihubungi.

Akibatnya bagi aplikasi seluler: simpan origin perusahaan, bukan pengenal tenant. Token yang diterbitkan sudah terikat pada satu perusahaan, sehingga menyimpan pengenalnya di sisi klien hanya menciptakan sumber kebenaran kedua yang dapat bertentangan dengan token.

Bentuk alamatContohKeterangan
Domain milik perusahaanos.perusahaan-contoh.idSatu host satu perusahaan. Bentuk yang paling lazim.
Portal bersamaportal.penyedia-contoh.idBanyak perusahaan berbagi satu host. Perusahaan ditentukan dari akun, bukan dari host.
Domain generikapp.sikaryawan.comTidak terhubung ke perusahaan mana pun.

Masuk dengan kode karyawan tidak berlaku pada portal bersama dan domain generik.

Kode karyawan hanya unik di dalam satu perusahaan. Pada host yang dipakai bersama, EMP-0001 bermakna ganda, sehingga /auth/employee-login menolak dengan pesan yang menyarankan memakai alamat perusahaan atau masuk dengan surel. Masuk dengan surel dan kata sandi tetap berjalan di semua bentuk alamat.

Autentikasi

Bearer JWT. Access token berlaku 30 menit, refresh token 14 hari.

POST/auth/employee-login Masuk dengan kode karyawan dan PIN. Tanpa token.
// Permintaan
{ "employee_code": "EMP-0010", "pin": "735291" }

// Balasan 200
{
  "tokens": {
    "access_token":  "eyJ0eXAiOi...",
    "refresh_token": "eyJ0eXAiOi...",
    "token_type":    "Bearer"
  },
  "tenant_id":       "a2e77511-fa3c-4527-9dda-84eaf9fb2ebe",
  "employee_id":     "6f1c...",
  "must_change_pin": false
}

must_change_pin bernilai benar ketika PIN baru saja disetel bagian SDM. Aplikasi wajib memaksa layar ganti PIN sebelum menampilkan apa pun yang lain.

Seluruh kegagalan identitas dijawab dengan pesan yang sama: "Kode karyawan atau PIN tidak cocok." Ini disengaja. Membedakan "kode tidak ada" dari "PIN salah" akan mengubah endpoint ini menjadi alat untuk memastikan kode karyawan mana yang berlaku di sebuah perusahaan. Jangan menerjemahkan ulang pesannya menjadi lebih spesifik di sisi aplikasi.

Penguncian. Lima kali salah berturut-turut mengunci akses selama 15 menit. Balasannya tetap 400, dengan pesan yang menyebutkan sisa waktunya. Perlakukan sebagai pesan untuk ditampilkan apa adanya, bukan sebagai kondisi yang perlu dikenali secara khusus.

POST/auth/login Masuk dengan surel dan kata sandi. Tanpa token.
// Permintaan
{ "email": "budi@contoh.id", "password": "...", "totp_code": "123456" }  // totp_code opsional

// Balasan 200 ketika verifikasi dua langkah masih diperlukan
{ "requires_2fa": true }

Cabang requires_2fa dijawab dengan status 200, bukan galat. Ini cabang yang paling sering terlupakan. Aplikasi yang hanya memeriksa status HTTP akan mengira masuknya berhasil lalu gagal membaca token.

POST/auth/refresh Menukar refresh token dengan sepasang token baru. Tanpa token pada header.
// Permintaan
{ "refresh_token": "eyJ0eXAiOi..." }

// Balasan 200. Perhatikan: tidak dibungkus "tokens"
{ "access_token": "...", "refresh_token": "...", "token_type": "Bearer" }

Penyegaran harus dijaga agar tidak berlomba. Bila satu layar memuat lima permintaan sekaligus dan seluruhnya menerima 401, penyegaran naif akan berjalan lima kali. Empat di antaranya memakai refresh token yang sudah dibuang.

Jagalah dengan satu penanda tunggal: permintaan kedua sampai kelima menunggu hasil penyegaran yang sama, lalu seluruhnya diulang dengan token baru. Permintaan penyegaran itu sendiri wajib memakai klien tanpa interseptor, karena 401 pada penyegaran akan memicu penyegaran lagi tanpa henti.

Konvensi balasan

Pembungkus daftar

Endpoint daftar membungkus isinya dalam data. Sebagian menyertakan halaman, sebagian tidak.

// Berhalaman: /attendance, /leave-requests, /reimbursements, /announcements, /briefings
{ "data": [ ... ], "page": 1, "per_page": 20, "total": 137 }

// Tanpa halaman: /attendance/shifts, /leave-types, /leave-balances,
//                /reimbursement-types, /hr/loans, /tasks/{id}/checklist,
//                /tasks/{id}/comments
{ "data": [ ... ] }

// Khusus /me/tasks dan /me/payslips
{ "data": [ ... ], "punya_data_karyawan": true }

punya_data_karyawan bernilai salah ketika akun yang masuk tidak tertaut ke data karyawan mana pun. Itu bukan galat: sebagian akun memang milik pengelola. Tampilkan keadaan kosong yang menjelaskan, bukan pesan gagal.

Galat

Seluruh galat memakai satu bentuk yang sama:

{ "error": { "code": "bad_request", "message": "Kode karyawan atau PIN tidak cocok." } }
StatuscodeArti bagi aplikasi
400bad_requestmessage ditulis untuk dibaca pemakai. Tampilkan apa adanya.
401unauthorizedToken kedaluwarsa atau tidak sah. Segarkan sekali, lalu ulangi.
403forbiddenIzin tidak mencukupi. Sering kali jawaban yang sah. Lihat Izin dan cakupan.
404not_foundTidak ada, atau ada tetapi di luar cakupan pemanggil.
409conflictBentrok, misalnya sudah absen masuk hari ini.
422tanpa pembungkusBadan JSON tidak dapat diurai menjadi bentuk yang diharapkan. Balasannya teks biasa dari kerangka kerja, bukan bentuk galat di atas.
500internal, db_errorPesannya sengaja umum. Rincian hanya masuk log server.

Tipe data

JenisBentukContoh
TanggalYYYY-MM-DD"2026-08-25"
Waktu penuhRFC 3339 UTC"2026-08-25T01:32:07Z"
Jam kerjaHH:MM:SS tanpa zona"08:00:00"
Uangbilangan bulat rupiah500000
PengenalUUID v4 sebagai teks"6f1c8a2e-..."

Tanggal kerja dan waktu penuh tidak sama. work_date adalah tanggal kalender menurut zona waktu perusahaan, sedangkan check_in adalah saat sesungguhnya dalam UTC. Mengambil tanggal dari check_in di sisi aplikasi akan meleset satu hari bagi shift malam.

Izin dan cakupan

Bagian ini menjelaskan mengapa endpoint yang sama menjawab berbeda bagi dua orang di perusahaan yang sama.

Setiap izin terdiri atas sumber daya, tindakan, dan cakupan. Cakupan berjenjang dari yang paling sempit: own hanya dirinya sendiri, lalu team, dept, branch, dan all untuk seisi perusahaan. Backend memakai cakupan terluas yang dimiliki, lalu menyaring baris menurut cakupan itu.

Setiap peran memiliki izin dasar sebagai karyawan. Hampir seluruhnya bercakupan own, sehingga tidak satu baris pun membuka data orang lain:

Sumber dayaTindakanCakupan
attendancecreate, readown
leavecreate, readown
reimbursementcreate, readown
payrollreadown
taskread, updateown
timesheetcreate, readown
employeereadown
filecreate, read, deleteown
productivityreadown
knowledge, commsreadall

403 sering merupakan jawaban yang benar, bukan kegagalan.

/briefings hanya ada di perusahaan yang memakai paket distribusi, dan hanya untuk pengemudi serta kernet. Bagi yang lain, 403 berarti "tidak berlaku bagi Anda". Ambillah endpoint semacam ini sebagai opsional, dan pastikan penolakannya tidak ikut menjatuhkan bagian layar yang sudah berhasil dimuat di sebelahnya.

Bedakan dengan tegas antara 403 dan kegagalan jaringan. Yang pertama tidak akan berubah bila diulang; yang kedua akan.

Aplikasi dapat membaca izin efektifnya sendiri dari GET /me, pada larik permissions dan denials. Periksa denials lebih dahulu: penolakan eksplisit mengalahkan izin apa pun.

Profil dan PIN

GET/public/branding Identitas visual perusahaan. Tanpa token.
// Ditemukan
{ "found": true, "name": "Perusahaan Contoh", "slug": "perusahaan-contoh-1234",
  "branding": { "logo_url": "...", "brand_color": "#4f46e5", "tagline": "..." } }

// Host portal bersama atau domain generik
{ "found": false, "portal": true }

Identitas visual adalah hiasan. Kegagalan mengambilnya tidak boleh menghalangi siapa pun masuk.

GET/me Profil, perusahaan, peran, izin, dan modul aktif.
{
  "user": { "id": "...", "email": "...", "name": "Operator Satu",
            "avatar_url": null, "totp_enabled": false, "preferences": {} },
  "employee_id": "...",
  "employee": { "id": "...", "code": "EMP-0010", "name": "Operator Satu",
                "email": "...", "phone": "..." },
  "tenant":  { "id": "...", "name": "Perusahaan Contoh", "slug": "perusahaan-contoh-1234",
               "logo_url": "...", "white_label": false,
               "hidden_nav": [], "connected_providers": ["ginee"] },
  "roles":       [ { "key": "production_operator", "name": "Operator Produksi" } ],
  "permissions": [ { "resource": "attendance", "action": "read", "scope": "own" } ],
  "denials":     [ { "resource": "payroll", "action": "read" } ],
  "modules":     [ { "module_key": "hr", "enabled": true } ],
  "is_platform_admin": false
}

employee bernilai null bila akunnya tidak tertaut ke data karyawan.

GET/me/app-pin Apakah pemakai sudah punya PIN.
{ "punya_data_karyawan": true, "employee_code": "EMP-0010", "punya_pin": true }
PUT/me/app-pin Mengganti PIN sendiri.
{ "pin_lama": "735291", "pin_baru": "486207" }

pin_lama boleh dihilangkan hanya ketika PIN sedang dalam keadaan wajib ganti karena baru disetel bagian SDM. Dalam keadaan itu pemiliknya baru saja membuktikan dirinya saat masuk, sehingga memintanya mengetik ulang PIN sementara hanya menambah langkah tanpa menambah bukti.

Aturan PIN: tepat enam angka, dan ditolak bila seluruh angkanya sama atau berurutan naik maupun turun. Kedua pola itu menyumbang bagian terbesar dari PIN yang dipakai orang, sehingga melarangnya menaikkan ketahanan jauh lebih banyak daripada menambah panjangnya. Terapkan pemeriksaan yang sama di sisi aplikasi agar pemakai tahu sebelum mengirim.

Kehadiran

GET/attendance Riwayat kehadiran. Berhalaman.

Parameter: from, to (tanggal), employee_id, page, per_page (baku 20, batas 100).

Tidak perlu mengirim employee_id. Backend sudah menyaring menurut cakupan izin pemanggil, sehingga karyawan biasa hanya menerima barisnya sendiri.

{ "data": [ {
    "id": "...", "employee_id": "...", "employee_name": "Operator Satu",
    "work_date": "2026-08-25",
    "check_in":  "2026-08-25T01:02:11Z",
    "check_out": "2026-08-25T10:04:52Z",
    "status": "present", "latitude": -6.21, "longitude": 106.84,
    "note": null, "shift_id": "...", "method": "mobile",
    "photo_file_id": null, "out_of_geofence": false
  } ], "page": 1, "per_page": 20, "total": 107 }

status bernilai present atau late. Terlambat ditentukan dari jam mulai shift ditambah toleransi.

GET/attendance/shifts Shift yang tersedia.
{ "data": [ { "id": "...", "name": "Pagi",
              "start_time": "08:00:00", "end_time": "17:00:00",
              "grace_minutes": 15 } ] }
POST/attendance/check-in Absen masuk.
POST/attendance/check-out Absen pulang.
{ "latitude": -6.21, "longitude": 106.84,
  "shift_id": "...",        // opsional; tanpa ini status selalu "present"
  "note": "...",            // opsional
  "method": "mobile",       // wajib cocok dengan metode yang diaktifkan perusahaan
  "photo_file_id": "..." }  // wajib bila perusahaan mensyaratkan foto

Balasannya satu baris kehadiran, bentuknya sama dengan di atas tanpa employee_name.

Pagar lokasi punya tiga mode, dan aplikasi harus menangani ketiganya.

off: lokasi diabaikan. warn: absen di luar area tetap diterima, tetapi barisnya ditandai out_of_geofence: true. enforce: absen di luar area ditolak 400, dan tidak adanya koordinat sama sekali juga ditolak.

Karena itu, kegagalan mengambil GPS tidak boleh diam-diam mengirim permintaan tanpa koordinat. Beri tahu pemakainya lebih dahulu, karena pesan penolakan dari server tidak akan menjelaskan bahwa penyebabnya ada di ponselnya sendiri.

Perusahaan juga dapat membatasi metode yang boleh dipakai. Bila mobile tidak diaktifkan, balasannya 400 dengan pesan yang menyebut metodenya.

Keadaan absen yang perlu ditangani terpisah:

KeadaanBalasanPesan
Absen masuk dua kali pada hari yang sama409duplicate value
Absen pulang tanpa pernah absen masuk400no check-in found for today
Absen pulang dua kali409already checked out today

Pesan 409 pada absen masuk berbunyi duplicate value, bukan kalimat yang layak dibaca pemakai. Pesan itu berasal dari pelanggaran batasan unik di basis data, bukan dari kalimat yang ditulis untuk manusia. Gantilah di sisi aplikasi menjadi kalimat yang menjelaskan, misalnya "Anda sudah absen masuk hari ini."

Pengajuan

Empat jenis pengajuan memakai pola yang sama: kirim, lalu tunggu keputusannya. Bacalah approval_status untuk cuti, lembur, dan reimbursement, serta status untuk kasbon.

Tanpa alur persetujuan yang dikonfigurasi, pengajuan disetujui otomatis tanpa pemberitahuan. approval_status yang bernilai null berarti pengajuannya tidak melalui alur persetujuan sama sekali, bukan berarti sedang menunggu. Bedakan keduanya di tampilan.

Cuti

GET/leave-types Jenis cuti beserta kuota tahunannya.
{ "data": [ { "id": "...", "name": "Cuti Tahunan", "quota_days": 12, "is_paid": true } ] }
GET/leave-balances Saldo cuti. Parameter: employee_id, leave_type_id, year.
{ "data": [ { "id": "...", "employee_id": "...", "leave_type_id": "...",
              "year": 2026, "entitled_days": 12, "used_days": 3 } ] }

Sisa cuti dihitung di sisi aplikasi: entitled_days - used_days. Tidak ada bidang remaining maupun balance.

Endpoint ini belum menyaring menurut cakupan izin. Tanpa parameter employee_id, balasannya memuat saldo seluruh karyawan di perusahaan, bukan hanya milik pemanggil. Lihat Masalah yang diketahui nomor 6.

Sampai diperbaiki, kirimkan selalu employee_id milik pemakai sendiri, dan jangan menampilkan baris yang bukan miliknya.

GET/leave-requests Daftar pengajuan cuti. Berhalaman.
POST/leave-requests Mengajukan cuti.
// Permintaan. employee_id dihilangkan berarti "atas nama saya"
{ "leave_type_id": "...", "start_date": "2026-09-01",
  "end_date": "2026-09-03", "reason": "Keperluan keluarga" }

// Balasan
{ "id": "...", "employee_id": "...", "employee_name": "Operator Satu",
  "leave_type_id": "...", "leave_type_name": "Cuti Tahunan",
  "start_date": "2026-09-01", "end_date": "2026-09-03", "days": 3,
  "reason": "...", "approval_request_id": "...", "approval_status": "pending" }

Lembur

GET/attendance/overtime Daftar lembur. Berhalaman.
POST/attendance/overtime Mengajukan lembur.
// Permintaan
{ "work_date": "2026-08-24", "minutes": 120, "reason": "Menyelesaikan pesanan" }

// Balasan
{ "id": "...", "employee_id": "...", "work_date": "2026-08-24",
  "minutes": 120, "reason": "...",
  "approval_request_id": "...", "approval_status": "pending" }

minutes wajib lebih dari nol.

Reimbursement

GET/reimbursement-types Jenis penggantian.
{ "data": [ { "id": "...", "name": "Transportasi", "created_at": "..." } ] }
GET/reimbursements Daftar pengajuan. Berhalaman.
POST/reimbursements Mengajukan penggantian.
// Permintaan
{ "type_id": "...", "amount": 150000, "description": "Ongkos kirim sampel",
  "receipt_url": "..." }   // opsional

// Balasan
{ "id": "...", "employee_id": "...", "type_id": "...", "amount": 150000,
  "description": "...", "receipt_url": null,
  "approval_request_id": "...", "approval_status": "pending",
  "current_step": 1, "created_at": "2026-08-25T02:10:00Z" }

Kasbon

GET/hr/loans Daftar kasbon. Menuntut izin payroll:read, tersaring menurut cakupan.
{ "data": [ { "id": "...", "employee_id": "...", "employee_name": "Operator Satu",
              "principal": 500000, "installments": 5,
              "installment_amount": 100000, "remaining": 300000,
              "start_period": "2026-09", "reason": "...",
              "status": "active", "paid_count": 2 } ] }
POST/hr/loans Mengajukan kasbon. Menuntut izin payroll:create.
// Seluruh bidang berikut WAJIB, termasuk employee_id dan start_period
{ "employee_id": "...", "principal": 500000, "installments": 5,
  "start_period": "2026-09", "reason": "Biaya sekolah anak" }

Endpoint ini belum dapat dipakai karyawan biasa. Berbeda dari tiga pengajuan lain, employee_id di sini wajib, dan izin yang dituntut adalah payroll:create yang tidak termasuk izin dasar karyawan. Lihat Masalah yang diketahui nomor 1.

Tugas

GET/me/tasks Tugas milik pemakai dari seluruh proyek.

Parameter: selesai (boolean, baku salah), per_page (baku 50, batas 200).

{ "data": [ { "id": "...", "project_id": "...", "project_name": "Produksi Agustus",
              "title": "Cek mesin lini 2", "description": "...",
              "status_name": "Berjalan", "due_date": "2026-08-26",
              "priority": "high", "selesai": false } ],
  "punya_data_karyawan": true }

Pakai endpoint ini, bukan /projects/{id}/tasks. Yang kedua menuntut pemanggilnya mengetahui proyeknya lebih dahulu, sehingga menyusun satu layar berarti belasan permintaan pada sambungan seluler.

selesai adalah tebakan, bukan keadaan yang tercatat. Basis data tidak menyimpan penanda selesai pada kolom papan; yang ada hanya nama dan urutannya. Backend menyimpulkannya dari nama kolom: done, selesai, completed, closed, finished, beres. Perusahaan yang menamai kolom terakhirnya di luar daftar itu akan melihat tugasnya tetap muncul sebagai pekerjaan berjalan.

GET/tasks/{task_id}/checklist Butir daftar periksa.
{ "data": [ { "id": "...", "task_id": "...", "label": "Periksa oli",
              "is_done": false, "position": 0 } ] }

Judul butir ada pada label, bukan title.

PATCH/tasks/{task_id}/checklist/{item_id}/toggle Membalik status satu butir. Hanya PATCH; POST menjawab 405.
GET/tasks/{task_id}/comments Komentar tugas, terlama lebih dahulu.
{ "data": [ { "id": "...", "task_id": "...",
              "author_membership_id": "...", "body": "Sudah dikerjakan",
              "created_at": "2026-08-25T03:00:00Z" } ] }

Yang dikirim adalah author_membership_id, bukan nama penulis. Nama harus dipetakan sendiri di sisi aplikasi.

POST/tasks/{task_id}/comments Menulis komentar.
{ "body": "Sudah dikerjakan", "mentions": ["membership-id-1"] }  // mentions opsional

Pengumuman dan briefing

GET/announcements Pengumuman perusahaan. Menuntut izin comms:read, dimiliki semua peran.

Parameter: is_active (boolean), page, per_page.

{ "data": [ { "id": "...", "title": "Libur bersama", "content": "...",
              "target_type": "all", "is_active": true,
              "author_membership_id": "...", "created_at": "..." } ],
  "page": 1, "per_page": 20, "total": 4 }
GET/briefings Briefing lapangan. Menuntut izin briefing:read.
POST/briefings/{id}/ack Menyatakan sudah membaca. Badan boleh memuat lat dan lng.

Ambil /briefings sebagai endpoint opsional. Izin briefing:read hanya diberikan pada paket distribusi, untuk pengemudi dan kernet. Bagi peran lain, 403 adalah jawaban yang benar dan berarti "tidak berlaku bagi Anda".

Bila layar Info mengambil pengumuman dan briefing berbarengan, tangani penolakan briefing sebagai daftar kosong. Tanpa itu, satu penolakan yang benar akan menjatuhkan pengumuman yang sudah berhasil diambil di sebelahnya.

Slip gaji

GET/me/payslips Slip gaji milik pemakai. Parameter per_page, baku 24, batas 120.
{ "data": [ { "id": "...", "run_id": "...", "period": "2026-08",
              "base": 5000000, "allowances": 750000, "deductions": 250000,
              "gross": 5750000, "net": 5500000, "tax": 120000 } ],
  "punya_data_karyawan": true }

Hanya periode penggajian yang sudah difinalisasi yang muncul, terbaru lebih dahulu.

Jangan menyusun layar ini dari /payroll/runs. Daftar periode penggajian bersifat seisi perusahaan dan tidak dapat disaring per orang, sehingga karyawan biasa memang menerima 403. /me/payslips menjawab pertanyaan yang sebenarnya, yaitu "slip saya yang mana saja", dan cakupannya terjaga sejak dari bentuk endpointnya. Satu permintaan, bukan tiga belas.

Notifikasi

POST/devices/fcm Mendaftarkan token perangkat.
DELETE/devices/fcm Mencabut token, misalnya saat keluar.
{ "token": "fcm-token", "platform": "android" }

Pendaftaran bersifat idempoten pada token: mengirim ulang token yang sama akan memperbarui barisnya, bukan menggandakannya. Token terikat pada perusahaan, pemakai, dan data karyawan yang sedang aktif, sehingga token wajib didaftarkan ulang setiap kali berpindah perusahaan dan dicabut saat keluar.

Pengiriman notifikasi belum menyala. Endpoint pendaftaran sudah berjalan dan token tersimpan, tetapi jalur pengiriman ke Firebase belum terpasang. Daftarkan tokennya sekarang agar tidak ada perubahan yang tertinggal, tetapi jangan menjanjikan notifikasi kepada pemakai.

Bekerja tanpa sinyal

Aplikasi ini dipakai di gudang, di jalan, dan di lokasi produksi. Sinyal hilang adalah keadaan biasa, bukan pengecualian.

Bedakan tiga jenis kegagalan, dan perlakukan berbeda:

JenisContohPerlakuan
Tidak tersambungTidak ada jaringan, waktu habisAntre, kirim ulang saat sinyal kembali.
Ditolak server400, 403, 409Laporkan saat itu juga. Jangan diantre.
Sesi habis401 setelah penyegaran gagalKeluarkan pemakai, bersihkan token.

Hanya kegagalan jaringan yang pantas diantre. Penolakan server, misalnya karena sudah absen hari ini, tidak akan berubah jawabannya bila diulang nanti. Menyembunyikannya di dalam antrean membuat karyawan mengira absennya berhasil, dan baru mengetahuinya keesokan hari.

Untuk permintaan yang diantre, sertakan kunci idempoten agar pengiriman ulang tidak menghasilkan pengajuan berganda. Endpoint baca dapat disimpan dengan kunci yang memuat parameternya, misalnya rentang tanggal, supaya permintaan dengan rentang berbeda tidak saling menimpa.

Masalah yang diketahui

Enam ketidakcocokan antara aplikasi karyawan yang berjalan sekarang dan backend, per 25 Agustus 2026. Nomor 1 dan 2 diuji langsung ke server; sisanya dibaca dari kode.

#GejalaSebabPerbaikan
1 Pengajuan kasbon selalu gagal Aplikasi mengirim {principal, installments, reason}, sedangkan backend mewajibkan employee_id dan start_period. Terbukti menjawab 422. Setelah itu masih tertahan 403 karena payroll:create bukan izin dasar karyawan. Longgarkan bentuk permintaan agar sejajar dengan cuti dan reimbursement, lalu tambahkan izinnya ke daftar dasar.
2 Mencentang daftar periksa tidak berpengaruh Aplikasi mengirim POST, rutenya hanya menerima PATCH. Terbukti menjawab 405. Ubah aplikasi menjadi PATCH.
3 Judul butir daftar periksa kosong Aplikasi membaca title, API mengirim label. Ubah pembacaan di aplikasi.
4 Penulis komentar selalu "Anggota tim" Aplikasi membaca author_name, API mengirim author_membership_id. Tambahkan nama penulis pada balasan API, dengan join seperti yang sudah dipakai di tempat lain.
5 Sisa kuota cuti selalu kosong Aplikasi membaca remaining atau balance; API mengirim entitled_days dan used_days. Hitung selisihnya di aplikasi, atau tambahkan remaining pada balasan API.
6 Saldo cuti seluruh karyawan terbaca siapa saja GET /leave-balances memanggil pemeriksaan izin tetapi membuang hasil cakupannya, dan kuerinya tidak memuat penyaring karyawan. Setiap peran memegang leave:read own. Terapkan penyaring cakupan seperti pada /leave-requests.

Nomor 6 adalah kebocoran data, bukan sekadar ketidakcocokan. Perbaikannya ada di backend dan tidak menunggu rilis aplikasi.

Berkas OpenAPI

Dua berkas OpenAPI 3.1, keduanya dapat diimpor ke Postman, Insomnia, atau pembangkit kode.

BerkasIsiKapan dipakai
openapi-lengkap.yaml 211 operasi, 161 jalur, 161 skema Untuk menyambungkan fitur baru. Dibangkitkan dari kode, jadi selalu mengikuti backend.
openapi.yaml 26 operasi yang dipakai aplikasi hari ini Untuk memahami permukaan yang sudah berjalan, dengan keterangan yang ditulis tangan.

Berkas lengkap dibangkitkan, berkas kecil ditulis tangan. Keduanya sengaja dipertahankan. Yang dibangkitkan menjamin tidak ada endpoint yang terlewat dan tidak akan menyimpang dari kode; yang ditulis tangan memuat penjelasan yang tidak dapat disimpulkan mesin, misalnya mengapa 403 pada briefing merupakan jawaban yang benar.

# Contoh membangkitkan klien Dart dari berkas lengkap
npx @openapitools/openapi-generator-cli generate \
  -i https://docs.sikaryawan.com/openapi-lengkap.yaml \
  -g dart-dio -o ./klien

Ganti {host} pada variabel server dengan host perusahaan yang dituju.