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_date
dan 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.
Comments