Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #9: Modul Transaksi Part 1

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #9: Modul Transaksi Part 1

Pendahuluan

Bagian terpenting dari sebuah aplikasi dengan kategori penjualan adalah modul transaksi, karena pada modul inilah semua data transaksi akan dicatat dengan banyak tujuan penggunaan, salah satunya adalah sebagai bahan untuk audit pemasukan dari pemilik aplikasi tersebut.

Serial yang membahas tentang transaksi akan kita bagi menjadi beberapa bagian karena menyangkut hal hal yang cukup kompleks dan banyak bagian yang ikut berperan dalam pembuatan modul ini. Sehingga pada bagian pertama ini kita akan berfokus membuat fitur untuk menambahkan data transaksi.

Adapun schema yang diinginkan adalah ketika membuat transaksi, user akan memilih customer terlebih dahulu dan secara otomatis akan menampilkan detail data dari customer tersebut. Selanjutnya adalah user akan menambahkan detail transaksi, meliputi service yan akang digunakan, dimana detail ini bisa lebih dari 1 service dalam 1 transaksi.

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vuejs - SPA) #8: Manage Customers

Add New Transaction

Routing akan menjadi langkah awal yang akan kita lakukan, buka file router.js dan tambahkan code berikut:

//[.. CODE SEBELUMNYA ..]
{
    path: '/transactions',
    component: IndexTransaction,
    meta: { requiresAuth: true },
    children: [
        {
            path: 'create',
            name: 'transactions.add',
            component: AddTransaction,
            meta: { title: 'Create New Transaction' }
        },
    ]
}
//[.. CODE SETELAHNYA ..]

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

import IndexTransaction from './pages/transaction/Index.vue'
import AddTransaction from './pages/transaction/Add.vue'

Langkah selanjutnya adalah membuat kedua file yang di-import, buat file Index.vue di dalam folder resources/js/pages/transaction dan tambahkan code:

<template>
    <div class="container">
        <section class="content-header">
            <h1>
                Manage Transaction
            </h1>
            <breadcrumb></breadcrumb>
        </section>

        <section class="content">
            <div class="row">
                <router-view></router-view>
            </div>
        </section>
    </div>
</template>
<script>
    import Breadcrumb from '../../components/Breadcrumb.vue'
    export default {
        name: 'IndexTransaction',
        components: {
            'breadcrumb': Breadcrumb
        }
    }
</script>

Note: Tidak ada bagian yang perlu dijelaskan pada code diatas.

File selanjutnya adalah Add.vue di dalam folder yang sama, tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Create New Transaction</h3>
            </div>
            <div class="panel-body">
                <transaction-form ref="form"></transaction-form>
                <div class="form-group">
                    <button class="btn btn-primary btn-sm btn-flat" @click.prevent="submit">
                        <i class="fa fa-save"></i> Create Transaction
                    </button>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    import { mapActions, mapState, mapMutations } from 'vuex'
    import FormTransaction from './Form.vue'
    export default {
        name: 'AddTransaction',
        methods: {
            //KETIKA TOMBOL CREATE TRANSACTION DITEKAN MAKA AKAN MENJALAN METHOD INI
            submit() {
                this.$refs.form.submit() //DIMANA KITA MENGINSTRUKSIKAN UNTUK MENJALANKAN METHOD submit() PADA FILE FORM.VUE MELALUI REFS
            }
        },
        components: {
            'transaction-form': FormTransaction
        }
    }
</script>

Code diatas meng-import sebuah file dengan nama Form.vue, buat file tersebut di dalam folder yang sama dan tambahkan code:

<template>
    <div class="row">
        <div class="col-md-6">
            <div class="form-group" :class="{ 'has-error': errors.customer_id }">
                <label for="">Customer</label>

                <!-- KITA AKAN MENGGUNAKAN V-SELECT DIMANA DATANYA AKAN DILOAD KETIKA KEYWORD PENCARIAN DITEMUKAN -->
                <v-select :options="customers.data"
                    v-model="transactions.customer_id"
                    @search="onSearch" 
                    label="name"
                    placeholder="Masukkan Kata Kunci" 
                    :filterable="false">
                    <template slot="no-options">
                        Masukkan Kata Kunci
                    </template>
                    <template slot="option" slot-scope="option">
                        {{ option.name }}
                    </template>
                </v-select>
                <p class="text-danger" v-if="errors.customer_id">{{ errors.customer_id[0] }}</p>
            </div>
        </div>
      	<!-- BAGIAN INI AKAN MENAMPILKAN DETAIL CUSTOMER JIKA ISFORM = FALSE, JIKA TRUE, PADA ARTIKEL SELANJUTNYA AKAN DIBUAT FORM UNTUK MENAMBAHKAN CUSTOMER BARU -->
        <div class="col-md-6" v-if="transactions.customer_id != null && !isForm">
            <table>
                <tr>
                    <th width="30%">NIK </th>
                    <td width="5%">:</td>
                    <td>{{ transactions.customer_id.nik }}</td>
                </tr>
                <tr>
                    <th>No Telp </th>
                    <td>:</td>
                    <td>{{ transactions.customer_id.phone }}</td>
                </tr>
                <tr>
                    <th>Alamat </th>
                    <td>:</td>
                    <td>{{ transactions.customer_id.address }}</td>
                </tr>
                <tr>
                    <th>Deposit </th>
                    <td>:</td>
                    <td>Rp {{ transactions.customer_id.deposit }}</td>
                </tr>
                <tr>
                    <th>Point </th>
                    <td>:</td>
                    <td>{{ transactions.customer_id.point }}</td>
                </tr>
            </table>
        </div>
        <div class="col-md-12">
            <hr>
            <button class="btn btn-warning btn-sm" style="margin-bottom: 10px" @click="addProduct">Tambah</button>
            <div class="table-responsive">
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th width="40%">Paket</th>
                            <th>Berat/Satuan</th>
                            <th>Harga</th>
                            <th>Subtotal</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                  	<!-- TABLE INI BERGUNA UNTUK MENAMBAHKAN ITEM TRANSAKSI -->
                    <tbody>
                        <tr v-for="(row, index) in transactions.detail" :key="index">
                            <td>
                                <v-select :options="products.data"
                                    v-model="row.laundry_price"
                                    @search="onSearchProduct" 
                                    label="name"
                                    placeholder="Masukkan Kata Kunci" 
                                    :filterable="false">
                                    <template slot="no-options">
                                        Masukkan Kata Kunci
                                    </template>
                                    <template slot="option" slot-scope="option">
                                        {{ option.name }}
                                    </template>
                                </v-select>
                            </td>
                            <td>
                                <div class="input-group">
                                    <input type="number" v-model="row.qty" class="form-control" @blur="calculate(index)">
                                    <span class="input-group-addon">{{ row.laundry_price != null && row.laundry_price.unit_type == 'Kilogram' ? 'gram':'pcs' }}</span>
                                </div>
                                
                            </td>
                            <td>Rp {{ row.price }}</td>
                            <td>Rp {{ row.subtotal }}</td>
                            <td>
                                <button class="btn btn-danger btn-flat" @click="removeProduct(index)">Hapus</button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
      
        <!-- KETIKA TRANSAKSI BERHASIL, ALERTNYA DITAMPILKAN -->
        <div class="col-md-12" v-if="isSuccess">
            <div class="alert alert-success">
                Transaksi Berhasil, Total Tagihan: Rp {{ total }}
            </div>
        </div>
    </div>
</template>

<script>
import { mapState, mapMutations, mapActions } from 'vuex'
import vSelect from 'vue-select'
import 'vue-select/dist/vue-select.css'
import _ from 'lodash'

export default {
    name: 'FormTransaction',
    data() {
        return {
            isForm: false,
            isSuccess: false,
            transactions: {
                customer_id: null,
                //KITA SET DEFAULT DETAILNYA 1 ITEM YANG KOSONG 
                detail: [
                    { laundry_price: null, qty: 1, price: 0, subtotal: 0 }
                ]
            }
        }
    },
    computed: {
        ...mapState(['errors']),
        ...mapState('transaction', {
            customers: state => state.customers, //GET STATE CUSTOMER DARI MODULE TRANSACTION
            products: state => state.products //GET STATE PRODUCT DARI MODULE TRANSACTION
        }),
        total() {
            //MENJUMLAH SUBTOTAL 
            return _.sumBy(this.transactions.detail, function(o) {
                return o.subtotal
            })
        }
    },
    methods: {
        ...mapActions('transaction', ['getCustomers', 'getProducts', 'createTransaction']),
        //METHOD INI AKAN BERJALAN KETIKA PENCARIAN DATA CUSTOMER PADA V-SELECT DIATAS
        onSearch(search, loading) {
            //KITA AKAN ME-REQUEST DATA CUSTOMER BERDASARKAN KEYWORD YG DIMINTA
            this.getCustomers({
                search: search,
                loading: loading
            })
        },
        //METHOD INI UNTUK PENCARIAN DATA PRODUK UNTUK ITEM LAUNDRY
        onSearchProduct(search, loading) {
            //ME-REQUEST DATA PRODUCT
            this.getProducts({
                search: search,
                loading: loading
            })
        },
        //KETIKA TOMBOL TAMBAHKAN DITEKAN, MAKA AKAN MENAMBAHKAN ITEM BARU
        addProduct() {
            this.transactions.detail.push({ laundry_price: null, qty: null, price: 0, subtotal: 0 })
        },
        //KETIKA TOMBOL HAPUS PADA MASING-MASING ITEM DITEKAN, MAKA AKAN MENGHAPUS BERDASARKAN INDEX DATANYA
        removeProduct(index) {
            if (this.transactions.detail.length > 1) {
                this.transactions.detail.splice(index, 1)
            }
        },
        //KETIKA INPTAN QTY / BERAT /SATUAN UN-FOCUS, MAKA FUNGSI INI AKAN DIJALANKAN
        calculate(index) {
            let data = this.transactions.detail[index]
            if (data.laundry_price != null) {
                //DIMANA KITA AKAN MENGISI PRICE UNTUK SETIAP ITEMNYA DAN PRICENYA DIDAPATKAN DARI DATA PRODUCT LAUNDRY
                data.price = data.laundry_price.price
                //ADAPUN SUBTOTAL AKAN DIHITUNG BERDASARKAN JENISNYA
                if (data.laundry_price.unit_type == 'Kilogram') {
                    //JIKA KILOGRAM MAKA BERAT BARANG * HARGA /1000
                    data.subtotal = (parseInt(data.laundry_price.price) * (parseInt(data.qty) / parseInt(1000))).toFixed(2)
                } else {
                    //JIKA SATUAN, MAKA HARGA * QTY
                    data.subtotal = parseInt(data.laundry_price.price) * parseInt(data.qty)
                }
            }
        },
        //KETIKA TOMBOL CREATE TRANSACTION DITEKAN MAKA FUNGSI INI AKAN DIJALANKAN
        submit() {
            this.isSuccess = false
            //MENGIRIM PERMINTAAN KE SERVER UNTUK MENYIMPAN DATA TRANSAKSI
            this.createTransaction(this.transactions).then(() => this.isSuccess = true)
        }
    },
    components: {
        vSelect
    }
}
</script>

Tugas selanjutnya adalah membuat Vuex module untuk meng-handle transaksi, buat file transaction.js di dalam folder stores dan tambahkan code:

import $axios from '../api.js'

const state = () => ({
    customers: [], //UNTUK MENAMPUNG DATA CUSTOMER YANG DI-REQUEST
    products: [], //UNTUK MENAMPUNG DATA PRODUCT YANG DI-REQUEST
    page: 1
})

const mutations = {
    //MENGUBAH STATE CUSTOMER BERDASARKAN DATA YANG DITERIMA
    ASSIGN_DATA(state, payload) {
        state.customers = payload
    },
    //MENGUBAH STATE PRODUCT BERDASARKAN DATA YANG DITERIMA
    DATA_PRODUCT(state, payload) {
        state.products = payload
    },
    SET_PAGE(state, payload) {
        state.page = payload
    }
}

const actions = {
    //MENGIRIM PERMINTAAN KE SERVER UNTUK MENGAMBIL DATA CUSTOMER BERDASARKAN KEYWORD YANG BERADA DI DALAM PAYLOAD.SEARCH
    getCustomers({ commit, state }, payload) {
        let search = payload.search
        payload.loading(true)
        return new Promise((resolve, reject) => {
            $axios.get(`/customer?page=${state.page}&q=${search}`)
            .then((response) => {
                //JIKA BERHASIL, SIMPAN DATANYA KE STATE
                commit('ASSIGN_DATA', response.data)
                payload.loading(false)
                resolve(response.data)
            })
        })
    },
    //MENGIRIM PERMINTAAN KE SERVER UNTUK MENGMABIL DATA PRODUCT, MEKANISMENYA SAMA DENGAN FUNGSI DIATAS
    getProducts({ commit, state }, payload) {
        let search = payload.search
        payload.loading(true)
        return new Promise((resolve, reject) => {
            $axios.get(`/product?page=${state.page}&q=${search}`)
            .then((response) => {
                //APABILA BERHASIL, SIMPAN KE STATE PRODUCTS
                commit('DATA_PRODUCT', response.data)
                payload.loading(false)
                resolve(response.data)
            })
        })
    },
    //FUNGSI UNTUK MEMBUAT TRANSAKSI
    createTransaction({commit}, payload) {
        return new Promise((resolve, reject) => {
            //MENGIRIM PERMINTAAN KE SERVER UNTUK MEMBUAT TRANSAKSI
            $axios.post(`/transaction`, payload)
            .then((response) => {
                resolve(response.data)
            })
        })
    }
}

export default {
    namespaced: true,
    state,
    actions,
    mutations
}

Agar module diatas dapat digunakan, buka file store.js dan register module diatas dengan menambahkan code berikut:

//[.. CODE SEBELUMNYA ..]
modules: {
    auth,
    outlet,
    courier,
    product,
    user,
    expenses,
    notification,
    customer,
    transaction //TAMBAHKAN BAGIAN INI
},
//[.. CODE SETELAHNYA ..]

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

import transaction from './stores/transaction.js'

Terakhir dari sisi client, definisikan link navigasi. Buka file resources/js/components/Header.vue dan tambahkan line berikut tepat dibawah link navigasi Expenses:

<li><router-link :to="{ name: 'transactions.add' }">Transaction</router-link></li>

Jangan lupa pada command line untuk meng-compile Vue.js nya dengan command:

npm run dev

Handle Transaction Request

Sebelum membuat code untuk meng-handle data transaksi, maka ada beberapa hal dari database yang perlu di-ubah. Pertama adalah menghapus courier_id karena bagian ini akan di-handle oleh user_id dan yang kedua adalah mengubah start_datedan end_date menjadi datetime & nullable karena terjadi kesalahan dimana pada data data product (table laundry_prices), kita tidak dapat mengindetifikasi lama pengerjaan untuk masing-masing paket. Sehingga bagian ini akan kita kerjakan pada artikel selanjutnya. Adapun courier_id, start_date dan end_date masing-masing berada pada table transactions.

Pada command line, buka migration dengan command:

php artisan make:migration change_date_null_to_transactions_table

Kemudian buka migrations baru tersebut dan modifikasi menjadi:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class ChangeDateNullToTransactionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('transactions', function (Blueprint $table) {
            //MENGUBAH TIPE DATA DAN NULLABLE
            //MENGGUNAKAN CHANGE PADA AKHIR STATEMENT KARENA FIELD TERSEBUT SUDAH ADA
            $table->datetime('start_date')->nullable()->change();
            $table->datetime('end_date')->nullable()->change();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('transactions', function (Blueprint $table) {
            //
        });
    }
}

Migration selanjutnya untuk menghapus courier_id, pada command line ketikkan perintah:

php artisan make:migration delete_courier_id_to_transactions_table

Buka 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 DeleteCourierIdToTransactionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('transactions', function (Blueprint $table) {
            $table->dropForeign(['courier_id']); //HAPUS FOREIGN KEY
            $table->dropColumn('courier_id'); //KEMUDIAN HAPUS FIELDNYA
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('transactions', function (Blueprint $table) {
            //
        });
    }
}

Terakhir, masih dengan command line, jalankan command:

php artisan migrate

Perkara database sudah diselesaikan, maka kita akan kembali ke topik pembahasan untuk meng-handle request yang diterima dari client. Generate controller baru dengan command:

php artisan make:controller API/TransactionController

Kemudian buka controller TransactionController.php dan modifikasi menjadi:

<?php

namespace App\Http\Controllers\API;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Transaction;
use App\DetailTransaction;
use DB;

class TransactionController extends Controller
{
    public function store(Request $request)
    {
        //VALIDASI
        $this->validate($request, [
            'customer_id' => 'required',
            'detail' => 'required'
        ]);

        //MENGGUNAKAN DATABASE TRANSACTION
        DB::beginTransaction();
        try {
            $user = $request->user(); //GET USER YANG SEDANG LOGIN
            //BUAT DATA TRANSAKSI
            $transaction = Transaction::create([
                'customer_id' => $request->customer_id['id'],
                'user_id' => $user->id,
                'amount' => 0
            ]);
            
            //KARENA DATA ITEM NYA LEBIH DARI SATU MAKA KITA LOOPING
            foreach ($request->detail as $row) {
                //DIMANA DATA YANG DITERIMA HANYALAH ITEM YANG LAUNDRY_PRICE (PRODUCT) NYA SUDAH DIPILIH
                if (!is_null($row['laundry_price'])) {
                    //MELAKUKAN PERHITUNGAN KEMBALI DARI SISI BACKEND UNTUK MENENTUKAN SUBTOTAL
                    $subtotal = $row['laundry_price']['price'] * $row['qty'];
                    if ($row['laundry_price']['unit_type'] == 'Kilogram') {
                        $subtotal = $row['laundry_price']['price'] * ($row['qty'] / 100);
                    }

                    //MENYIMPAN DATA DETAIL TRANSAKSI
                    DetailTransaction::create([
                        'transaction_id' => $transaction->id,
                        'laundry_price_id' => $row['laundry_price']['id'],
                        'laundry_type_id' => $row['laundry_price']['laundry_type_id'],
                        'qty' => $row['qty'],
                        'price' => $row['laundry_price']['price'],
                        'subtotal' => $subtotal
                    ]);
                }
            }
            //APABILA TIDAK TERJADI ERROR, MAKA KITA COMMIT AGAR BENAR2 MENYIMPAN DATANYA
            DB::commit();
            return response()->json(['status' => 'success']);
        } catch (\Exception $e) {
            DB::rollback(); //JIKA TERJADI ERROR, MAKA DIROLLBACK AGAR DATA YANG BERHASIL DISIMPAN DIHAPUS
            return response()->json(['status' => 'error', 'data' => $e->getMessage()]);
        }
    }
}

Definisikan routing untuk API-nya, buka file routes/api.php dan tambahkan code:

Route::resource('transaction', 'API\TransactionController')->except(['create', 'show']);

Terakhir adalah membuat data permission menggunakan $fillable atau $guarded pada masing-masing model terkait. Buka file app/Transaction.php dan modifikasi menjadi:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
    protected $guarded = []; //TAMBAHKAN LINE INI
}

Lakukan hal yang sama dengan model app/DetailTransaction.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class DetailTransaction extends Model
{
    protected $guarded = [];
}

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vuejs - SPA) #7: Push Notifications Expenses

Kesimpulan

Bagian pertama dari seri membuat aplikasi Laundry menggunakan Laravel & Vue.js yang membahas tentang transaksi berhasil diselesaikan, namun masih memiliki banyak bagian yang harus dilengkapi. Setidaknya pada bagian ini kita telah belajar banyak hal diantaranya, membuat database transaction laravel, bekerja dengan lebih dari 1 table (model), membuat dynamic form vue.js, menggunakan lodash, menggunakan v-select, menampilkan alert dan lain sebagainya.

Adapun dokumentasi code dari artikel ini dapat kamu lihat di Github.

Category:
Share:

Comments