Pendahuluan
Setelah menyelesaikan fitur untuk mencatat transaksi yang telah dilakukan oleh user, maka tugas selanjutnya adalah membuat fitur untuk meng-input data pembayaran. Setiap transaksi memiliki 1 data pembayaran yang berarti customer harus membayar lunas setiap transaksinya.
Adapun Mekanisme pembayaran yang akan digunakan adalah dengan menggunakan versi Cash atau Via Deposit. Namun pada artikel ini kita akan membahas versi Cash terlebih dahulu dengan memiliki dua buah opsi apabila jumlah pembayarannya lebih besar dari tagihan. Pertama, kembaliannya akan dikembalikan secara tunai dan opsi kedua adalah dimasukkan ke dalam deposit customer terkait.
Selain itu, fitur tambahannya adalah kasir (user) bisa menandai (mengubah) status masing-masing item transaksi menjadi selesai ketika customer mengambil pesanannya. Setiap item transaksi akan mendapatkan 1 point ketika transaksi tersebut telah dianggap selesai.
Baca Juga: Aplikasi Laundry Laravel 5.8 & Vuejs #11: Modul Transaksi Part 3
Detail Transaksi
Bagian pertama yang akan diselesaikan adalah fitur untuk menampilkan detail pesanan, buka file router.js
dan definisikan route berikut:
{
path: '/transactions',
component: IndexTransaction,
meta: { requiresAuth: true },
children: [
{
path: 'create',
name: 'transactions.add',
component: AddTransaction,
meta: { title: 'Create New Transaction' }
},
//TAMBAHKAN ROUTE INI
{
path: 'view/:id',
name: 'transactions.view',
component: ViewTransaction,
meta: { title: 'View Transaction' }
},
]
}
Jangan lupa import component-nya di dalam file yang sama:
import ViewTransaction from './pages/transaction/View.vue'
Selanjutnya buat file View.vue
di dalam folder /pages/transaction
dan tambahkan code:
<template>
<div class="col-md-12">
<div class="panel">
<div class="panel-body">
<div class="row">
<div class="col-md-6" v-if="transaction.status == 0">
<!-- FORM PEMBAYARAN -->
</div>
<div class="col-md-6" v-if="transaction.customer">
<!-- MENAMPILKAN DETAIL INFORMASI CUSTOMER TERKAIT -->
<h4>Customer Info</h4>
<hr>
<table>
<tr>
<th width="30%">NIK </th>
<td width="5%">:</td>
<td>{{ transaction.customer.nik }}</td>
</tr>
<tr>
<th>No Telp </th>
<td>:</td>
<td>{{ transaction.customer.phone }}</td>
</tr>
<tr>
<th>Alamat </th>
<td>:</td>
<td>{{ transaction.customer.address }}</td>
</tr>
<tr>
<th>Deposit </th>
<td>:</td>
<td>Rp {{ transaction.customer.deposit }}</td>
</tr>
<tr>
<th>Point </th>
<td>:</td>
<td>{{ transaction.customer.point }}</td>
</tr>
</table>
</div>
<div class="col-md-6" v-if="transaction.payment">
<!-- MENAMPILKAN RIWAYAT PEMBAYARAN ORDERAN TERSEBUT -->
<h4>Riwayat Pemabayaran</h4>
<hr>
<table>
<tr>
<th width="30%">Jumlah Pembayaran </th>
<td width="5%">:</td>
<td>Rp {{ transaction.payment.amount }}</td>
</tr>
<tr>
<th>Kembalian </th>
<td>:</td>
<td>Rp {{ transaction.payment.customer_change }}</td>
</tr>
<tr>
<th>Metode Pembayaran </th>
<td>:</td>
<td>{{ transaction.payment.type_label }}</td>
</tr>
</table>
</div>
<div class="col-md-12" style="padding-top: 20px">
<div class="alert alert-success" v-if="payment_success">Pembayaran Berhasil</div>
<h4>Detail Transaction</h4>
<hr>
<div class="table-responsive">
<table class="table table-hover table-bordered">
<thead>
<tr>
<th>Paket</th>
<th width="28%">Waktu Layanan</th>
<th>Berat/Satuan</th>
<th>Harga</th>
<th>Subtotal</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- LOOPING DETAIL TRANSAKSI -->
<tr v-for="(row, index) in transaction.detail" :key="index">
<td>
<strong>{{ row.product.name }}</strong>
<sup v-html="row.status_label"></sup>
</td>
<td>{{ row.service_time }}</td>
<td>
{{ row.qty }} ({{ row.product.unit_type }})
</td>
<td>Rp {{ row.price }} / {{ row.product.unit_type }}</td>
<td>Rp {{ row.subtotal }}</td>
<td>
<!-- TOMBOL UNTUK MENYELESAIKAN SETIAP PESANAN -->
<!-- TOMBOL INI DITAMPILKAN KETIKA PEMBAYARAN SUDAH DILAKUKAN DAN STATUSNYA MASIH PROSES -->
<button class="btn btn-success btn-sm" v-if="transaction.status == 1 && row.status == 0" @click="isDone(row.id)">
<!-- KETIKA DIKLIK MAKA AKAN MENJALANKAN FUNGSI isDone() DAN MENGIRIMKAN PARAMETER ID DETAIL TRANSAKSI -->
<i class="fa fa-paper-plane-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
export default {
name: 'DetailTransaction',
created() {
//KETIKA COMPONENT DI-LOAD, MAKA KITA LOAD DATA TRANSAKSI BERDASARKAN ID TRANSAKSI YANG DI DAPATKAN DARI URL
this.detailTransaction(this.$route.params.id)
},
data() {
//DEFINISIKAN VARIABLE YANG NNTINYA DIGUNAKAN UNTUK PAYMENT
return {
amount: null,
customer_change: false,
loading: false,
payment_message: null,
payment_success: false
}
},
computed: {
...mapState('transaction', {
//MENGAMBIL DATA TRANSAKSI YANG TELAH DISIMPAN KE DALAM STATE TRANSACTION
transaction: state => state.transaction
})
},
methods: {
...mapActions('transaction', ['detailTransaction', 'completeItem']),
//KETIKA TOMBOL MASING-MASING PESANAN DIKLIK
isDone(id) {
//MAKA KITA MENAMPILKAN ALERT KONFIRMASI
this.$swal({
title: 'Kamu Yakin?',
text: "Akan menyelesaikan pesanan ini!",
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Iya, Lanjutkan!'
}).then((result) => {
if (result.value) {
//JIKA SETUJU MAKA KIRIM PERMINTAAN KE SERVER
this.completeItem({ id: id }).then(() => {
//JIKA BERHASIL, MAKA LOAD DATA TRASANSAKSI TERBARU
this.detailTransaction(this.$route.params.id)
})
}
})
}
}
}
</script>
Sebelum membuat API untuk meng-handle request yang dikirimkan, maka kita modifikasi module Vuex transaction.js
. Buka file tersebut dan tambahkan state & mutations berikut:
const state = () => ({
customers: [],
products: [],
transaction: [], //TAMBAHKAN STATE INI
page: 1
})
const mutations = {
ASSIGN_DATA(state, payload) {
state.customers = payload
},
DATA_PRODUCT(state, payload) {
state.products = payload
},
SET_PAGE(state, payload) {
state.page = payload
},
//TAMBAHKAN MUTATIONS INI
ASSIGN_TRANSACTION(state, payload) {
state.transaction = payload
},
}
Masih dengan file yang sama, pada bagian actions tambahkan code berikut:
detailTransaction({ commit }, payload) {
//MENGIRIM PERMINTAAN KE SERVER UNTUK MENGAMBIL DATA BERDASARKAN ID TRANSAKSI
return new Promise((resolve, reject) => {
$axios.get(`/transaction/${payload}/edit`)
.then((response) => {
//DATANYA KITA SIMPAN KE DALAM STATE TRANSACTION MENGGUNAKAN MUTATION
commit('ASSIGN_TRANSACTION', response.data.data)
resolve(response.data)
})
})
},
completeItem({ commit }, payload) {
return new Promise((resolve, reject) => {
$axios.post(`/transaction/complete-item`, payload)
.then((response) => {
resolve(response.data)
})
})
}
Kita tinggalkan dari sisi client-nya, karena tugas selanjutnya adalah membuat API untuk mengambil data dan mengubah status masing-masing item pesanan. Buka file TransactionController
dan tambahkan method:
public function edit($id)
{
//LAKUKAN QUERY KE DATABASE UNTUK MENCARI ID TRANSAKSI TERKAIT
//KITA JUGA ME-LOAD DATA LAINNYA MENGGUNAKAN EAGER LOADING, MAKA SELANJUTNYA AKAN DI DEFINISIKAN FUNGSINYA
$transaction = Transaction::with(['customer', 'payment', 'detail', 'detail.product'])->find($id);
return response()->json(['status' => 'success', 'data' => $transaction]);
}
public function completeItem(Request $request)
{
//VALIDASI UNTUK MENGECEK IDNYA ADA ATAU TIDAK
$this->validate($request, [
'id' => 'required|exists:detail_transactions,id'
]);
//LOAD DATA DETAIL TRANSAKSI BERDASARKAN ID
$transaction = DetailTransaction::with(['transaction.customer'])->find($request->id);
//UPDATE STATUS DETAIL TRANSAKSI MENJADI 1 YANG BERARTI SELESAI
$transaction->update(['status' => 1]);
//UPDATE DATA CUSTOMER TERKAIT DENGAN MENAMBAHKAN 1 POINT
$transaction->transaction->customer()->update(['point' => $transaction->transaction->customer->point + 1]);
return response()->json(['status' => 'success']);
}
Karena ada fungsi untuk me-load data lainnya menggunakan eager loading, maka kita akan mendefinisikan fungsinya masing-masing. Buka file Transaction.php
dan tambahkan method berikut:
public function detail()
{
//TRANSAKSI KE DETAIL MENGGUNAKAN RELASI ONE TO MANY
return $this->hasMany(DetailTransaction::class);
}
public function customer()
{
//TRANSAKSI KE CUSTOMER MELAKUKAN REFLEK DATA TERKAIT MENGGUAKAN BELONGSTO
return $this->belongsTo(Customer::class);
}
Selanjutnya buka file DetailTransaction.php
dan modifikasi menjadi:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class DetailTransaction extends Model
{
protected $guarded = [];
protected $dates = ['start_date', 'end_date'];
protected $appends = ['service_time', 'status_label']; //AGAR ATTRIBUTE BARU TERSEBUT MUNCUL DI DALAM JSON, MAKA APPEND NAMA ATTRIBUTENYA. Contoh: ServiceTime menjadi service_time. kata GET dan ATTRIBUTE dibuang
//KITA BUAT ATTRIBUTE BARU UNTUK SERVICE_TIME
public function getServiceTimeAttribute()
{
//ISINYA ADALAH START DATE DAN END DATE DI REFORMAT SESUAI TANGGAL INDONESIA
return $this->start_date->format('d-m-Y H:i:s') . ' s/d ' . $this->end_date->format('d-m-Y H:i:s');
}
//BUAT ATTRIBUTE BARU UNTUK LABEL STATUS
public function getStatusLabelAttribute()
{
if ($this->status == 1) {
return '<span class="label label-success">Selesai</span>';
}
return '<span class="label label-default">Proses</span>';
}
//RELASI KE TABLE TRANSACTIONS
public function transaction()
{
return $this->belongsTo(Transaction::class);
}
//RELASI KE LAUNDRY_PRICES
public function product()
{
return $this->belongsTo(LaundryPrice::class, 'laundry_price_id');
}
}
Field status
belum dibuat pada table detail_transactions
, maka kita perlu membuat migrations untuk meng-generate field tersebut. Pada command line, jalankan perintah:
php artisan make:migration add_field_status_to_detail_transactions_table
Buka file migration yang telah dibuat dan modifikasi menjadi:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFieldStatusToDetailTransactionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('detail_transactions', function (Blueprint $table) {
//STATUS MENGGUNAKAN BOOLEAN DENGAN NILAI DEFAULT FALSE (0)
$table->boolean('status')->after('subtotal')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('detail_transactions', function (Blueprint $table) {
$table->dropColumn('status');
});
}
}
Jangan lupa jalankan command:
php artisan migrate
Terakhir definisikan routing API-nya, buka file routes/api.php
dan tambahkan code:
Route::post('transaction/complete-item', 'API\TransactionController@completeItem');
Payment Module
Bagian yang belum diselesaikan adalah form pembayaran yang berguna untuk meng-input data pembayaran tentunya. Kembali ke file View.vue
, tempatkan code berikut di dalam tag html yang sudah kita berikan komentar sebelumnya.
<div class="col-md-6" v-if="transaction.status == 0">
<h4>Payment</h4>
<hr>
<div class="form-group">
<label for="">Tagihan</label>
<input type="text" :value="transaction.amount" class="form-control" readonly>
</div>
<div class="form-group">
<label for="">Jumlah Bayar</label>
<input type="number" class="form-control" v-model="amount">
</div>
<p v-if="isCustomerChange">Kembalian: Rp {{ customerChangeAmount }}</p>
<div class="form-group" v-if="isCustomerChange">
<input type="checkbox" v-model="customer_change" id="customer_change">
<label for="customer_change"> Kembalian Jadi Deposit?</label>
</div>
<p class="text-danger" v-if="payment_message">{{ payment_message }}</p>
<button class="btn btn-primary btn-sm" :disabled="loading" @click="makePayment">Bayar</button>
</div>
Masih dengan file yang sama, buat methods dan computed property berikut:
//[.. CODE SEBELUMNYA ..]
computed: {
...mapState('transaction', {
transaction: state => state.transaction
}),
//TAMBAHKAN KEDUA CODE DIBAWAH INI
isCustomerChange() {
return this.amount > this.transaction.amount //BERNILAI TRUE/FALSE SESUAI KONDISINYA
},
customerChangeAmount() {
return parseInt(this.amount - this.transaction.amount) //SELISIH ANTARA TAGIHAN DAN JUMLAH YANG DIBAYARKAN
}
},
methods: {
...mapActions('transaction', ['detailTransaction', 'payment', 'completeItem']),
makePayment() {
//JIKA JUMLAH PEMBAYARAN KURANG DARI TAGIHAN
if (this.amount < this.transaction.amount) {
//MAKA SET NOTIF ERROR
this.payment_message = 'Pembayaran Kurang Dari Tagihan'
//HENTIKAN PROSES
return
}
//SELAIN ITU, MAKA SET LOADING JADI TRUE
this.loading = true
//BUAT REQUEST KE SERVER
this.payment({
//DENGAN MENGIRIMKAN PARAMETER BERIKUT
transaction_id: this.$route.params.id,
amount: this.amount,
customer_change: this.customer_change
}).then(() => {
//SET BAHWA PAYMENT BERHASIL, DIGUNAKAN OLEH ALERT NNTINYA
this.payment_success = true
setTimeout(() => {
//SET LOADING JADI FALSE KEMBALI
this.loading = false
//SET SEMUA VARIABLE JADI KOSONG
this.amount = null,
this.customer_change = false,
this.payment_message = null
}, 500)
//AMBIL DATA TRANSAKSI TERBARU
this.detailTransaction(this.$route.params.id)
})
},
// [.. CODE SETELAHNYA ..]
}
Buat actions untuk meng-handle permintaan ke server, buka file transaction.js
dan tambahkan actions berikut:
payment({ commit }, payload) {
return new Promise((resolve, reject) => {
$axios.post(`/transaction/payment`, payload)
.then((response) => {
resolve(response.data)
})
})
},
Dari sisi client sudah selesai, maka tugas selanjutnya adalah membuat API untuk menyimpan informasi pembayaran ke database. Buka file TransactionController.php
dan tambahkan method:
public function makePayment(Request $request)
{
//VALIDASI REQUEST
$this->validate($request, [
'transaction_id' => 'required|exists:transactions,id',
'amount' => 'required|integer'
]);
DB::beginTransaction();
try {
//CARI TRANSAKSI BERDASARKAN ID
$transaction = Transaction::find($request->transaction_id);
//SET DEFAULT KEMBALI = 0
$customer_change = 0;
if ($request->customer_change) {
//JIKA CUSTOMER_CHANGE BERNILAI TRUE
$customer_change = $request->amount - $transaction->amount; //MAKA DAPATKAN BERAPA BESARAN KEMBALIANNYA
//TAMBAHKAN KE DEPOSIT CUSTOMER
$transaction->customer()->update(['deposit' => $transaction->customer->deposit + $customer_change]);
}
//SIMPAN INFO PEMBAYARAN
Payment::create([
'transaction_id' => $transaction->id,
'amount' => $request->amount,
'customer_change' => $customer_change,
'type' => false
]);
//UPDATE STATUS TRANSAKSI JADI 1 BERARTI SUDAH DIBAYAR
$transaction->update(['status' => 1]);
//JIKA TIDAK ADA ERROR, COMMIT PERUBAHAN
DB::commit();
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
return response()->json(['status' => 'failed', 'data' => $e->getMessage()]);
}
}
Dengan file yang sama, tambahkan use statement:
use App\Payment;
Buat API untuk pembayaran, buka file routes/api.php
dan tambahkan code:
Route::post('transaction/payment', 'API\TransactionController@makePayment');
Bagian yang terpenting adalah membuat model dan migration untuk table payments
. Pada command line, jalankan perintah:
php artisan make:model Payment -m
Buka file migration yang baru saja di-generate dan modifikasi menjadi:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePaymentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('payments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('transaction_id');
$table->integer('amount');
$table->integer('customer_change');
$table->boolean('type')->default(false)->comment('0: Cash, 1: Deposit');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('payments');
}
}
Pastikan untuk mengizinkan mass assignment, buka file Payment.php
dan modifikasi menjadi:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
protected $guarded = [];
protected $appends = ['type_label']; //APPEND KE JSON OBJECT
//MEMBUAT ATTRIBUTE BARU
public function getTypeLabelAttribute()
{
if ($this->type == 0) {
return 'Cash';
}
return 'Deposit';
}
}
Terakhir, buat fungsi untuk relasi antara transactions
dan payments
. Buka file Transaction.php
dan tambahkan code:
public function payment()
{
return $this->hasOne(Payment::class);
}
Create Transaction to Detail Transaction
Ketika transaksi berhasil dibuat, ada bagian yang terlupakan yakni tombol untuk mengarahkan user ke detail transaksi. Buka file /transaction/Form.vue
dan modifikas beberapa bagian. Pertama bagian alert tagihan, ubah menjadi:
<div class="col-md-12" v-if="isSuccess">
<div class="alert alert-success">
Transaksi Berhasil, Total Tagihan: Rp {{ total }}
<!-- TAMBAHKAN CODE INI -->
<strong><router-link :to="{ name: 'transactions.view', params: {id: transaction_id} }">Lihat Detail</router-link></strong>
</div>
</div>
Kemudian pada bagian property data()
, tambahkan variable:
transaction_id: null,
Lalu method submit()
modifikasi menjadi
submit() {
this.isSuccess = false
let filter = _.filter(this.transactions.detail, function(item) {
return item.laundry_price != null
})
if (filter.length > 0) {
//MODIFIKASI BAGIAN INI
this.createTransaction(this.transactions).then((res) => {
this.transaction_id = res.data.id
this.isSuccess = true
})
}
},
Tentu saja pada bagian akhir adalah kita perlu mengirimkan data transaksi yang baru saja dibuat. Buka file TransactionController.php
, fokus ke method store()
dan pada bagian response() success, modifikasi menjadi:
return response()->json(['status' => 'success', 'data' => $transaction]);
Jangan lupa untuk menjalankan npm run dev
atau npm run watch
ketika melakukan perubahan pada sisi client.
Baca Juga: Aplikasi Laundry Laravel 5.8 & Vuejs #10: Modul Transaksi Part 2
Kesimpulan
Menampilan detail transaksi, melakukan pembayaran dan mengubahan status detail transaksi ketika customer mengambil pesanan adalah bagian yang telah kita kerjakan pada artikel ini. Tentu saja kita telah belajar banyak hal, salah satunya ada berinteraksi lebih dari 1 table dalam waktu bersamaan.
Adapun dokumentasi code artikel ini bisa kamu lihat di Github.
Comments