Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #13: List Transaksi

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #13: List Transaksi

Pendahuluan

Melihat rekap dari semua transaksi yang telah dilakukan adalah satu satu fitur yang penting dari sebuah aplikasi berbasis penjualan. Pada seri belajar Membuat Aplikasi Laundry Laravel 5.8 & Vue.js, dimana kita telah memasuki seri ke-13 akan membahas bagaimana menampilkan semua data transaksi yang ada.

Selain itu, kita juga akan melengkapi fitur yang masih kurang sempurna. Materi ini sebenarnya adalah pengulangan karena teknik yang digunakan sama saja dengan teknik yang telah kita pelajari sebelumnya. Adapun materi yang akan kita bahas diantaranya, bagaimana berinteraksi lebih dari 1 table, menggunakan database transaction, membuat filter berdasarkan interaksi user, dan lain sebagainya.

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

List of Transactions

Seluruh riwayat transaksi wajib ditampilkan sebagi informasi untuk memudahkan pengguna dalam mencari atau menulusuri keigatan yang telah dilakukannya selama menggunakan aplikasi tersebut. Maka kita akan menyediakan fitur tersebut yang disertai filter agar memudahkan user mencari informasi sesuai yang diinginkannya.

Pertama, kita selesaikan dari sisi backend dimana hal yang akan dilakukan adalah membuat API yang berisi data transaksi. Buka file TransactionController.php dan tambahkan sebuah method:

public function index()
{
    $search = request()->q; //TAMPUNG QUERY PENCARIAN DARI URL
    $user = request()->user(); //GET USER YANG SEDANG LOGIN
  
    //BUAT QUERY KE DATABASE DENGAN ME-LOAD RELASI TABLE TERKAIT DAN DIURUTKAN BERDASARKAN CREATED_AT
    //whereHas() DIGUNAKAN UNTUK MEN-FILTER NAMA CUSTOMER YANG DICARI USER, AKAN TETAPI NAMA TERSEBUT BERADA PADA TABLE CUSTOMERS
    //PARAMETER PERTAMA DARI whereHas() ADALAH NAMA RELASI YANG DIDEFINISIKAN DIMODEL
    $transaction = Transaction::with(['user', 'detail', 'customer'])->orderBy('created_at', 'DESC')
        ->whereHas('customer', function($q) use($search) {
            $q->where('name', 'LIKE', '%' . $search . '%');
        });

  //JIKA FILTERNYA ADALAH 0 DAN 1. DIMANA 0 = PROSES, 1 = SELESAI DAN 2 = SEMUA DATA
    if (in_array(request()->status, [0,1])) {
        //MAKA AMBIL DATA BERDASARKAN STATUS TERSEBUT
        $transaction = $transaction->where('status', request()->status);
    }
  
    //JIKA ROLENYA BUKAN SUPERADMIN
    if ($user->role != 0) {
        //MAKA USER HANYA AKAN MENDAPATKAN TRANSAKSI MILIKNYA SAJA
        $transaction = $transaction->where('user_id', $user->id);
    }
    $transaction = $transaction->paginate(10);
    return new TransactionCollection($transaction);
}

Jangan lupa tambahkan use statement di dalam file yang sama:

use App\Http\Resources\TransactionCollection;

Pada command line, jalankan command berikut untuk men-generate file TransactionCollection.php.

php artisan make:resource TransactionCollection

Buka file yang baru saja di-generate, kemudian modifikasi menjadi

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class TransactionCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection
        ];
    }
}

Bagian terakhir dari proses pembuatan API adalah dengan mendifinisikan Eloquent relationships, dimana sebelumnya kita telah me-load relasi user, customer dan detail. Dari ketiga relasi tersebut, user belum terdefinisikan, maka buka file Transaction.php dan tambahkan code berikut:

protected $appends = ['status_label']; //APPEND ACCESSORNYA AGAR DITAMPILKAN DIJSON YANG DIRETURN

//INI ADALAH ACCESSOR UNTUK CUSTOM FIELD STATUS YANG AKAN DIAPPEND KE JSON
public function getStatusLabelAttribute()
{
    //JIKA STATUS NYA 1 
    if ($this->status == 1) {
        //MAKA VALUENYA ADALAH HTML YANG BERISI LABEL SUCCESS
        return '<span class="label label-success">Selesai</span>';
    }
    //SELAIN ITU MENAMPILKAN LABEL PRIMARY
    return '<span class="label label-primary">Proses</span>';
}

//BUAT RELASI ANTARA USER DAN TRANSACTION
public function user()
{
    return $this->belongsTo(User::class);
}

Data yang dibutuhkan untuk ditampilkan telah siap, maka tugas kita selanjutnya adalah mengkonsumsi data tersebut sehingga user dapat melihatnya. Buat routing-nya terlebih dahulu, buka file router.js dan tambahkan code berikut

{
    path: '/transactions',
    component: IndexTransaction,
    meta: { requiresAuth: true },
    children: [
        {
            path: 'create',
            name: 'transactions.add',
            component: AddTransaction,
            meta: { title: 'Create New Transaction' }
        },
        {
            path: 'view/:id',
            name: 'transactions.view',
            component: ViewTransaction,
            meta: { title: 'View Transaction' }
        },
      
        //TAMBAHKAN CODE INI
        {
            path: 'list',
            name: 'transactions.list',
            component: ListTransaction,
            meta: { title: 'List Transaction' }
        },
        //TAMBAHKAN CODE INI
    ]
}

Masih dengan file yang sama, pada bagian import statement, tambahkan code:

import ListTransaction from './pages/transaction/List.vue'

Kemudian buat file List.vue di dalam folder resources/js/pages/transaction dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <router-link :to="{ name: 'transactions.add' }" class="btn btn-primary btn-sm btn-flat">Add New</router-link>
                <div class="pull-right">
                    <div class="row">
                      
                      	<!-- FORM UNTUK FILTER BERDASARKAN STATUS -->
                        <div class="col-md-6">
                            <select v-model="filter_status" class="form-control">
                                <option value="2">All</option>
                                <option value="1">Selesai</option>
                                <option value="0">Proses</option>
                            </select>
                        </div>
                      
                        <!-- FORM UNTUK MELAKUKAN PENCARIAN -->
                        <div class="col-md-6">
                            <input type="text" class="form-control" placeholder="Cari..." v-model="search">
                        </div>
                    </div>
                </div>
            </div>
            <div class="panel-body">
              
                <!-- SEPERTI SEBELUMNYA, TABLE INI AKAN MENAMPILKAN DATA TRANSAKSI -->
                <!-- ADAPUN PENJELASANNYA SAMA DENGAN PENJELASAN SEBELUMNYA -->
                <b-table striped hover bordered :items="transactions.data" :fields="fields" show-empty>
                    <template slot="customer" slot-scope="row">
                        <p><strong>{{ row.item.customer ? row.item.customer.name:'' }}</strong></p>
                        <p>Telp: {{ row.item.customer.phone }}</p>
                        <p>NIK: {{ row.item.customer.nik }}</p>
                    </template>
                    <template slot="user_id" slot-scope="row">
                        <p>{{ row.item.user ? row.item.user.name:'' }}</p>
                    </template>
                    <template slot="service" slot-scope="row">
                        <p>{{ row.item.detail.length }} Item</p>
                    </template>
                    <template slot="amount" slot-scope="row">
                        <p>Rp {{ row.item.amount }}</p>
                    </template>
                    <template slot="status" slot-scope="row">
                        <p v-html="row.item.status_label"></p>
                    </template>
                    <template slot="actions" slot-scope="row">
                        <router-link :to="{ name: 'transactions.view', params: {id: row.item.id} }" class="btn btn-info btn-sm"><i class="fa fa-eye"></i></router-link>
                    </template>
                </b-table>

                <div class="row">
                    <div class="col-md-6">
                        <p v-if="transactions.data"><i class="fa fa-bars"></i> {{ transactions.data.length }} item dari {{ transactions.meta.total }} total data</p>
                    </div>
                    <div class="col-md-6">
                        <div class="pull-right">
                            <b-pagination
                                v-model="page"
                                :total-rows="transactions.meta.total"
                                :per-page="transactions.meta.per_page"
                                aria-controls="transactions"
                                v-if="transactions.data && transactions.data.length > 0"
                                ></b-pagination>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { mapActions, mapState } from 'vuex'

export default {
    name: 'DataTransaction',
    created() {
        //KETIKA COMPONENT DI-LOAD MAKA FUNGSI INI AKAN DIJALANKAN
        this.getTransactions({
            status: this.filter_status,
            search: this.search
        })
    },
    data() {
        return {
            //DEFINISIKAN FIELD YANG AKAN DITAMPILKAN PADA TABLE DIATAS
            fields: [
                { key: 'id', label: 'Order ID' },
                { key: 'customer', label: 'Customer' },
                { key: 'user_id', label: 'Admin' },
                { key: 'service', label: 'Item Jasa' },
                { key: 'amount', label: 'Total' },
                { key: 'created_at', label: 'Tgl Transaksi' },
                { key: 'status', label: 'Status' },
                { key: 'actions', label: 'Aksi' }
            ],
            search: '',
            filter_status: 2 //DEFAULTNYA KITA SET 2 = ALL
        }
    },
    computed: {
        //AMBIL DATA DARI STATE LIST_TRANSACTION
        ...mapState('transaction', {
            transactions: state => state.list_transaction
        }),
        //AMBIL DATA PAGE YANG AKTIF
        page: {
            get() {
                return this.$store.state.transaction.page
            },
            set(val) {
                this.$store.commit('transaction/SET_PAGE', val)
            }
        }
    },
    watch: {
        //JIKA PAGE BERUBAH VALUENYA
        page() {
            //MAKA GET DATA CUSTOMER YANG BARU BERDASARKAN PAGE
            this.getTransactions({
                status: this.filter_status,
                search: this.search
            })
        },
        //JIKA SEARCH VALUENYA BERUBAH
        search() {
            //MAKA GET CUSTOMER BARU BERDASARKAN FILTER SEARCH
            this.getTransactions({
                status: this.filter_status,
                search: this.search
            })
        },
        //JIKA FILTER_STATUS VALUENYA BERUBAH
        filter_status() {
            //MAKA GET DATA CUSTOMER YANG BARU BERDASARKAN FILTERNYA
            this.getTransactions({
                status: this.filter_status,
                search: this.search
            })
        }
    },
    methods: {
        ...mapActions('transaction', ['getTransactions']) 
    }
}
</script>

Bagian terakhir adalah mendefinisikan actions dari Vuex dan state-nya. Buka file transaction.js dan tambahkan method berikut tepat didalam actions.

getTransactions({ commit, state }, payload) {
    let search = typeof payload.search != 'undefined' ? payload.search:''
    let status = typeof payload.status != 'undefined' ? payload.status:''
    return new Promise((resolve, reject) => {
        $axios.get(`/transaction?page=${state.page}&q=${search}&status=${status}`)
        .then((response) => {
            commit('ASSIGN_DATA_TRANSACTION', response.data)
            resolve(response.data)
        })
    })
},

Masih dengan file yang sama, pada bagian mutations tambahkan code:

ASSIGN_DATA_TRANSACTION(state, payload) {
    state.list_transaction = payload
},

Dan terakhir pada file transaction.js, tambahkan state berikut:

list_transaction: [],

Modifikasi Navigation Menu

Transaksi akan kita kelompokkan menjadi dropdown seperti settings, maka buka file Header.vue dan modifikasi tag untuk menu transaksi menjadi:

<li class="dropdown">
    <a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="true">Transactions <span class="caret"></span></a>
    <ul class="dropdown-menu" role="menu">
        <li><router-link :to="{ name: 'transactions.list' }">List</router-link></li>
        <li><router-link :to="{ name: 'transactions.add' }">Add New</router-link></li>
    </ul>
</li>

Bayar Via Deposit

Jika sebelumnya kita telah membuat fitur untuk pembayaran pada setiap transaksi menggunakan sistem cash, maka bagian ini akan disediakan opsi pembayaran via deposit apabila deposit customer terkait sama dengan atau lebih dari total tagihan.

Buka file js/pages/transaction/View.vue dan dan modifikas beberapa bagian berikut:

<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>
  
  	<!-- TAMBAHKAN CODINGAN INI -->
  	<!-- JADI OPSI CHECKBOX INI HANYA AKAN DITAMPILKAN JIKA KONDISI DEPOSITNYA TERSEDIA -->
    <div class="form-group" v-if="transaction.customer && transaction.customer.deposit >= transaction.amount">
        <input type="checkbox" v-model="via_deposit"> Bayar Via Deposit?
    </div>
    <!-- TAMBAHKAN CODINGAN INI -->
  
    <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>

Menggunakan file yang sama, tambahkan property watch dan modifikasi bagian lainnya:

data() {
    return {
        amount: null,
        customer_change: false,
        loading: false,
        payment_message: null,
        payment_success: false,
        via_deposit: false //TAMBAHKAN VARIABLE INI
    }
},
watch: {
    //JIKA VALUE NYA BERUBAH
    via_deposit() {
        //CEK JIKA TRUE
        if (this.via_deposit) {
            //MAKA TOTAL PEMBAYARAN DISET SEJUMLAH TAGIHAN
            this.amount = this.transaction.amount
        } else {
            //JIKA FALSE MAKA DI SET NULL KEMBALI
            this.amount = null
        }
    }
},
computed: {
    ...mapState('transaction', {
        transaction: state => state.transaction
    }),
      
    //MODIFIKASI BAGIAN INI
    isCustomerChange() {
        //JIKA VIA DEPOSIT TRUE
        if (!this.via_deposit) {
            //MAKA JALANKAN KONDISI SEBELUMNYA
            return this.amount > this.transaction.amount 
        }
        //SELAIN ITU NILAINYA false
        return false
    },
    customerChangeAmount() {
        //JIKA VIA_DEPOSIT TRUE
        if (!this.via_deposit) {
            //MAKA VALUENYA ADALAH HASIL PENGURANGAN JUMLAH BAYAR - TOTAL TAGIHAN
            return parseInt(this.amount - this.transaction.amount)
        }
        //DEFAULT NILAINYA ADALAH 0
        return 0
    }
    //MODIFIKASI BAGIAN INI
},

Masih dengan file yang sama, modifikasi methods yang meng-handle pembayaran.

makePayment() {
    if (this.amount < this.transaction.amount) {
        this.payment_message = 'Pembayaran Kurang Dari Tagihan'
        return
    }
    this.loading = true
    this.payment({
        transaction_id: this.$route.params.id,
        amount: this.amount,
        customer_change: this.customer_change,
        via_deposit: this.via_deposit //TAMBAHKAN BAGIAN INI 
    }).then((res) => {
        //JIKA PEMBAYARAN BERHASIL
        if (res.status == 'success') {
            //ALERT DAN SEMUA VARIABLE DI RESET
            this.payment_success = true
            setTimeout(() => {
                this.loading = false
                this.amount = null,
                this.customer_change = false,
                this.payment_message = null
                this.via_deposit = false
            }, 500)
            this.detailTransaction(this.$route.params.id)
        } else {
            //JIKA GAGAL MAKA TAMPILKAN ALERT GAGAL
            this.loading = false
            alert(res.data)
        }
    })
},

Kemudian pada sisi backend, buka file TransactionController.php dan modifikasi method makePayment() menjadi.

public function makePayment(Request $request)
{
    $this->validate($request, [
        'transaction_id' => 'required|exists:transactions,id',
        'amount' => 'required|integer'
    ]);

    DB::beginTransaction();
    try {
        $transaction = Transaction::with(['customer'])->find($request->transaction_id);

        $customer_change = 0;
        //LAKUKAN PENGECEKAN, JIKA VIA_DEPOSIT TRUE
        if ($request->via_deposit) {
            //MAKA DI CEK LAGI, JIKA DEPOSIT CUSTOMER KURANG DARI TOTAL TAGIHAN
            if ($transaction->customer->deposit < $request->amount) {
                //MAKA KIRIM RESPONSE KALO ERROR PEMBAYARANNYA
                return response()->json(['status' => 'error', 'data' => 'Deposit Kurang!']);
            }
          
            //SELAIN ITU, MAKA PERBAHARUI DEPOSIT CUSTOMER
            $transaction->customer()->update(['deposit' => $transaction->customer->deposit - $request->amount]);
          
        //JIKA VALUE VIA_DEPOSIT FALSE (VIA CASH)
        } else {
            //MAKA DI CEK LAGI, JIKA ADA KEMBALIANNYA
            if ($request->customer_change) {
                //DAPATKAN KEMBALIAN
                $customer_change = $request->amount - $transaction->amount;
                
                //DAN TAMBAHKAN KE DEPOSIT CUSTOMER
                $transaction->customer()->update(['deposit' => $transaction->customer->deposit + $customer_change]);
            }
        }

        Payment::create([
            'transaction_id' => $transaction->id,
            'amount' => $request->amount,
            'customer_change' => $customer_change,
            'type' => $request->via_deposit //UBAH BAGIAN TIPE PEMBAYARANNYA, 0 = CASH, 1 = DEPOSIT
        ]);
        $transaction->update(['status' => 1]);
        DB::commit();
        return response()->json(['status' => 'success']);
    } catch (\Exception $e) {
        return response()->json(['status' => 'failed', 'data' => $e->getMessage()]);
    }
}

Bagian terakhir adalah meng-compile Vue.js nya dengan menjalankan command npm run dev

Baca Juga: Aplikasi Qur'an Digital & Play Audio Menggunakan Flutter

Kesimpulan

Sebagai penutup dari modul transaksi, maka pada artikel ini kita melengkapi apa yang kurang dari fitur sebelumnya, seperti pembayaran via deposit, menampilkan list transaksi yang telah dilakukan oleh user, dan mengelompokkan menu transaksi menjadi dropdown menu.

Bagian baru yang kita pelajari pada artikel ini adalah cara menggunakan whereHas() untuk men-filter data yang berelasi. Adapun dokumentasi dari artikel ini dapat kamu temukan di Github.

Category:
Share:

Comments