Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #12: Payment & Detail Transaksi

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #12: Payment & Detail Transaksi

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.

Category:
Share:

Comments