Tutorial Android #7: Dashboard Petugas – Form Transaksi Setor dan Tarik Tunai
Halo para teller bank! Sekarang kita akan membuat dashboard petugas yang sesungguhnya. Di sini petugas bisa melayani setor dan tarik tunai nasabah. Kita akan buat form input yang keren, validasi jumlah, dan mengirim data ke API Laravel yang sudah kita bangun. Siap jadi teller digital?
🎯 Apa yang akan kita lakukan?
- ✅ Membuat
PetugasDashboardActivitydengan dua tab atau dua card untuk setor dan tarik. - ✅ Mendesain layout dengan EditText untuk ID rekening, jumlah, keterangan, dan button.
- ✅ Menambahkan validasi input (tidak kosong, jumlah minimal 1000).
- ✅ Menambahkan endpoint di ApiService untuk deposit dan withdraw.
- ✅ Mengirim data ke API menggunakan Retrofit dengan menyertakan token.
- ✅ Menampilkan loading pada button saat memproses.
- ✅ Menampilkan notifikasi sukses/gagal dengan Toast.
- ✅ Membersihkan form setelah transaksi berhasil.
Langkah 1: Membuat Activity dan Layout
Buat activity baru: PetugasDashboardActivity.java (jika belum ada dari tutorial #6). Kita akan gunakan layout dengan dua card vertikal atau dua bagian.
Layout activity_petugas_dashboard.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#f5f5f5">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#28a745"
app:title="Dashboard Petugas"
app:titleTextColor="#fff"
app:navigationIcon="?attr/homeAsUpIndicator"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Card Setor Tunai -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="💰 Setor Tunai"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#28a745"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDepositAccountId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="ID Rekening"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDepositAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Jumlah (Rp)"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDepositDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Keterangan (opsional)"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnDeposit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Setor"
android:textAllCaps="false"
android:backgroundTint="#28a745"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Card Tarik Tunai -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="💸 Tarik Tunai"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#dc3545"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etWithdrawAccountId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="ID Rekening"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etWithdrawAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Jumlah (Rp)"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etWithdrawDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Keterangan (opsional)"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnWithdraw"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tarik"
android:textAllCaps="false"
android:backgroundTint="#dc3545"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</LinearLayout>
Langkah 2: Menambahkan Model untuk Response (BaseResponse)
Kita akan menggunakan BaseResponse yang sudah dibuat di tutorial #5 untuk menampung pesan dari server. Jika belum ada, buat class BaseResponse.java:
package com.example.banktabunganapp;
import com.google.gson.annotations.SerializedName;
public class BaseResponse {
@SerializedName("message")
private String message;
public String getMessage() {
return message;
}
}
📞 Langkah 3: Menambahkan Endpoint di ApiService
Buka ApiService.java dan tambahkan method untuk deposit dan withdraw:
// Deposit
@FormUrlEncoded
@POST("deposit")
Call<BaseResponse> deposit(
@Header("Authorization") String token,
@Field("account_id") int accountId,
@Field("amount") double amount,
@Field("description") String description
);
// Withdraw
@FormUrlEncoded
@POST("withdraw")
Call<BaseResponse> withdraw(
@Header("Authorization") String token,
@Field("account_id") int accountId,
@Field("amount") double amount,
@Field("description") String description
);
Langkah 4: Implementasi PetugasDashboardActivity
Buka PetugasDashboardActivity.java dan isi dengan kode berikut:
package com.example.banktabunganapp;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.google.android.material.textfield.TextInputEditText;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class PetugasDashboardActivity extends AppCompatActivity {
private TextInputEditText etDepositAccountId, etDepositAmount, etDepositDescription;
private TextInputEditText etWithdrawAccountId, etWithdrawAmount, etWithdrawDescription;
private Button btnDeposit, btnWithdraw;
private SessionManager sessionManager;
private ApiService apiService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_petugas_dashboard);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
sessionManager = new SessionManager(this);
apiService = ApiClient.getService();
// Cek session
if (!sessionManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Inisialisasi view
etDepositAccountId = findViewById(R.id.etDepositAccountId);
etDepositAmount = findViewById(R.id.etDepositAmount);
etDepositDescription = findViewById(R.id.etDepositDescription);
btnDeposit = findViewById(R.id.btnDeposit);
etWithdrawAccountId = findViewById(R.id.etWithdrawAccountId);
etWithdrawAmount = findViewById(R.id.etWithdrawAmount);
etWithdrawDescription = findViewById(R.id.etWithdrawDescription);
btnWithdraw = findViewById(R.id.btnWithdraw);
btnDeposit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
processDeposit();
}
});
btnWithdraw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
processWithdraw();
}
});
}
private void processDeposit() {
String accountIdStr = etDepositAccountId.getText().toString().trim();
String amountStr = etDepositAmount.getText().toString().trim();
String description = etDepositDescription.getText().toString().trim();
// Validasi
if (TextUtils.isEmpty(accountIdStr)) {
etDepositAccountId.setError("ID Rekening harus diisi");
return;
}
if (TextUtils.isEmpty(amountStr)) {
etDepositAmount.setError("Jumlah harus diisi");
return;
}
int accountId = Integer.parseInt(accountIdStr);
double amount = Double.parseDouble(amountStr);
if (amount < 1000) {
etDepositAmount.setError("Minimal Rp 1.000");
return;
}
// Disable button dan ubah teks
btnDeposit.setEnabled(false);
btnDeposit.setText("Memproses...");
String token = sessionManager.getToken();
String authHeader = "Bearer " + token;
Call call = apiService.deposit(authHeader, accountId, amount, description);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
btnDeposit.setEnabled(true);
btnDeposit.setText("Setor");
if (response.isSuccessful()) {
Toast.makeText(PetugasDashboardActivity.this, response.body().getMessage(), Toast.LENGTH_SHORT).show();
// Kosongkan form
etDepositAccountId.setText("");
etDepositAmount.setText("");
etDepositDescription.setText("");
} else {
if (response.code() == 401) {
sessionManager.logout();
startActivity(new Intent(PetugasDashboardActivity.this, LoginActivity.class));
finish();
} else {
try {
String errorMsg = response.errorBody().string();
Toast.makeText(PetugasDashboardActivity.this, "Error: " + errorMsg, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(PetugasDashboardActivity.this, "Transaksi gagal", Toast.LENGTH_SHORT).show();
}
}
}
}
@Override
public void onFailure(Call call, Throwable t) {
btnDeposit.setEnabled(true);
btnDeposit.setText("Setor");
Toast.makeText(PetugasDashboardActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private void processWithdraw() {
String accountIdStr = etWithdrawAccountId.getText().toString().trim();
String amountStr = etWithdrawAmount.getText().toString().trim();
String description = etWithdrawDescription.getText().toString().trim();
if (TextUtils.isEmpty(accountIdStr)) {
etWithdrawAccountId.setError("ID Rekening harus diisi");
return;
}
if (TextUtils.isEmpty(amountStr)) {
etWithdrawAmount.setError("Jumlah harus diisi");
return;
}
int accountId = Integer.parseInt(accountIdStr);
double amount = Double.parseDouble(amountStr);
if (amount < 1000) {
etWithdrawAmount.setError("Minimal Rp 1.000");
return;
}
btnWithdraw.setEnabled(false);
btnWithdraw.setText("Memproses...");
String token = sessionManager.getToken();
String authHeader = "Bearer " + token;
Call call = apiService.withdraw(authHeader, accountId, amount, description);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
btnWithdraw.setEnabled(true);
btnWithdraw.setText("Tarik");
if (response.isSuccessful()) {
Toast.makeText(PetugasDashboardActivity.this, response.body().getMessage(), Toast.LENGTH_SHORT).show();
etWithdrawAccountId.setText("");
etWithdrawAmount.setText("");
etWithdrawDescription.setText("");
} else {
if (response.code() == 401) {
sessionManager.logout();
startActivity(new Intent(PetugasDashboardActivity.this, LoginActivity.class));
finish();
} else {
try {
String errorMsg = response.errorBody().string();
Toast.makeText(PetugasDashboardActivity.this, "Error: " + errorMsg, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(PetugasDashboardActivity.this, "Transaksi gagal", Toast.LENGTH_SHORT).show();
}
}
}
}
@Override
public void onFailure(Call call, Throwable t) {
btnWithdraw.setEnabled(true);
btnWithdraw.setText("Tarik");
Toast.makeText(PetugasDashboardActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_dashboard, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_profile) {
startActivity(new Intent(this, ProfileActivity.class));
return true;
} else if (item.getItemId() == R.id.action_logout) {
showLogoutConfirmation();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showLogoutConfirmation() {
new AlertDialog.Builder(this)
.setTitle("Konfirmasi Logout")
.setMessage("Apakah Anda yakin ingin keluar?")
.setPositiveButton("Ya", (dialog, which) -> {
sessionManager.logout();
startActivity(new Intent(this, LoginActivity.class));
finish();
})
.setNegativeButton("Batal", null)
.show();
}
@Override
public boolean onSupportNavigateUp() {
finish();
return true;
}
}
Langkah 5: Uji Coba
Pastikan server Laravel berjalan dan endpoint /api/deposit dan /api/withdraw sudah tersedia (tutorial #5). Login sebagai petugas, lalu coba lakukan setor dan tarik dengan data valid. Perhatikan Toast yang muncul.
- Jika berhasil, form akan kosong dan muncul pesan sukses.
- Jika jumlah kurang dari 1000, muncul error validasi.
- Jika rekening tidak ditemukan atau saldo tidak cukup, server akan mengembalikan error dan ditampilkan di Toast.
- Jika token expired, akan logout otomatis.
auth:sanctum dan isPetugas agar hanya petugas yang bisa mengakses. Juga pastikan validasi saldo dll sudah ditangani di server.
Kesimpulan
- ✅ Dashboard petugas dengan dua form transaksi telah selesai.
- ✅ Validasi client-side mencegah input salah.
- ✅ Integrasi Retrofit berjalan lancar dengan pengiriman token.
- ✅ Notifikasi memberi feedback ke petugas.
- ✅ Penanganan error dan token expired sudah diterapkan.
Di tutorial selanjutnya (#8: Dashboard Petugas – Lihat Mutasi Nasabah) kita akan menambahkan fitur pencarian mutasi nasabah. Sampai jumpa!
Daftar Tutorial Android (Lanjutan)
- #8: Dashboard Petugas – Lihat Mutasi Nasabah
- #9: Dashboard Admin – Manajemen Pengguna
- #10: Laporan Transaksi dengan Filter dan Ekspor PDF