Pendahuluan
Inti dari sebuah aplikasi point of sales adalah transaksi, sebab tujuannya adalah untuk mencatat seluruh proses penjualan yang dilakukan oleh pengguna. Maka pada bagian ini kita akan menganalisa bersama apa saja data yang diperlukan untuk menyelesaikan aplikasi yang sedang kita bangun. Perlu untuk di-ingat bahwa ini adalah artikel yang saling terkait, maka bagi teman-teman yang baru bergabung, saya harap untuk memulainya dari awal (baca part 1), agar komentar terkait kendala yang ditemukan sesuai.
Pada seri pertama artikel ini, kita telah membuat sebuah schema database dimana telah terdapat table: Orders, Order_details, dan Customer.
Baca Juga: Membuat Aplikasi POS (Point of Sales) Laravel 5.6 - Role & Permission Users
Form Transaksi
Tahap pertama dalam menyelesaikan proses transaksi adalah dengan menyediakan form yang akan digunakan oleh kasir, schema-nya, form ini hanya bisa diakses oleh user yang memiliki role kasir. Buat controller dengan command:
php artisan make:controller OrderController
Buka file OrderController.php
kemudian tambahkan method:
public function addOrder()
{
$products = Product::orderBy('created_at', 'DESC')->get();
return view('orders.add', compact('products'));
}
Jangan lupa menambahkan use statement:
use App\Product;
Buat file add.blade.php
didalam folder resources/views/orders
, kemudian tambahkan code berikut:
@extends('layouts.master')
@section('title')
<title>Transaksi</title>
@endsection
@section('css')
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" />
@endsection
@section('content')
<div class="content-wrapper">
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">Transaksi</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active">Transaksi</li>
</ol>
</div>
</div>
</div>
</div>
<section class="content" id="dw">
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
@card
@slot('title')
@endslot
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="">Produk</label>
<select name="product_id" id="product_id" class="form-control" required width="100%">
<option value="">Pilih</option>
@foreach ($products as $product)
<option value="{{ $product->id }}">{{ $product->code }} - {{ $product->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="">Qty</label>
<input type="number" name="qty" id="qty" value="1" min="1" class="form-control">
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm">
<i class="fa fa-shopping-cart"></i> Ke Keranjang
</button>
</div>
</div>
<!-- MENAMPILKAN DETAIL PRODUCT -->
<div class="col-md-5">
<h4>Detail Produk</h4>
<div v-if="product.name">
<table class="table table-stripped">
<tr>
<th>Kode</th>
<td>:</td>
<td>@{{ product.code }}</td>
</tr>
<tr>
<th width="3%">Produk</th>
<td width="2%">:</td>
<td>@{{ product.name }}</td>
</tr>
<tr>
<th>Harga</th>
<td>:</td>
<td>@{{ product.price | currency }}</td>
</tr>
</table>
</div>
</div>
<!-- MENAMPILKAN IMAGE DARI PRODUCT -->
<div class="col-md-3" v-if="product.photo">
<img :src="'/uploads/product/' + product.photo"
height="150px"
width="150px"
:alt="product.name">
</div>
</div>
@slot('footer')
@endslot
@endcard
</div>
</div>
</div>
</section>
</div>
@endsection
@section('js')
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/accounting.js/0.4.1/accounting.min.js"></script>
<script src="{{ asset('js/transaksi.js') }}"></script>
@endsection
Note: Terdapat dua cdn library yang di-load yakni: select2 (untuk membuat select box bisa melakukan filtering. Doc) dan accounting (untuk melakukan formatting currency). File js terakhir adalah custom js, yang akan dibuat menggunakan Vue.js.
Karena kita akan menggunakan Laravel mix untuk men-generate file js yang dibuatkan, buat file transaksi.js
di dalam folder resources/assets/js
, kemudian tambahkan code berikut:
import Vue from 'vue'
import axios from 'axios'
Vue.filter('currency', function (money) {
return accounting.formatMoney(money, "Rp ", 2, ".", ",")
})
new Vue({
el: '#dw',
data: {
product: {
id: '',
qty: '',
price: '',
name: '',
photo: ''
}
},
watch: {
//apabila nilai dari product > id berubah maka
'product.id': function() {
//mengecek jika nilai dari product > id ada
if (this.product.id) {
//maka akan menjalankan methods getProduct
this.getProduct()
}
}
},
//menggunakan library select2 ketika file ini di-load
mounted() {
$('#product_id').select2({
width: '100%'
}).on('change', () => {
//apabila terjadi perubahan nilai yg dipilih maka nilai tersebut
//akan disimpan di dalam var product > id
this.product.id = $('#product_id').val();
});
},
methods: {
getProduct() {
//fetch ke server menggunakan axios dengan mengirimkan parameter id
//dengan url /api/product/{id}
axios.get(`/api/product/${this.product.id}`)
.then((response) => {
//assign data yang diterima dari server ke var product
this.product = response.data
})
}
}
})
Penjelasan: Vue.filter
digunakan untuk melakukan formatting currency dengan menggunakan library accounting.js, hal ini telah dibahas dalam artikel Filtering & Formatting Vue.js.
Karena terdapat fungsi untuk melakukan fetch ke server setiap kali product dipilih, maka kita perlu membuat sebuah methods untuk menghandle request tersebut. Sebelum melanjutkannya, perlu diketahui bersama bahwa schema yang diinginkan adalah ketika product dipilih maka akan melakukan request untuk mengambil data product dari database berdasarkan product yang pilih, kemudian ditampilkan detail product tersebut. Buka file OrderController.php
kemudian tambahkan method:
public function getProduct($id)
{
$products = Product::findOrFail($id);
return response()->json($products, 200);
}
Gunakan method yang baru saja dibuat di dalam route api, buka file routes/api.php
kemudian tambahkan:
Route::get('/product/{id}', 'OrderController@getProduct');
Sedangkan untuk method addOrder
, tambahkan ke dalam routes/web.php
:
...
Route::group(['middleware' => ['role:kasir']], function() {
Route::get('/transaksi', 'OrderController@addOrder')->name('order.transaksi');
});
...
Penjelasan: Sebagaimana yang telah disebutkan diawal bahwa transaksi ini hanya dapat dilakukan oleh Kasir, maka URI /transaksi
kita protect dengan middleware role kasir. Agar dapat mengakses halaman ini, silahkan buat user baru dengan role kasir, kemudian login dengan user tersebut.
Tambahkan page ini pada sidebar menu, buka resources/views/layouts/module/sidebar.blade.php
, kemudian tambahkan:
...
@role('kasir')
<li class="nav-item">
<a href="{{ route('order.transaksi') }}" class="nav-link">
<i class="nav-icon fa fa-shopping-cart"></i>
<p>
Transaksi
</p>
</a>
</li>
@endrole
...
Apabila kita login dengan user yang telah memiliki role kasir dan mengakses uri /transaksi
, maka tampilan yang akan kita peroleh adalah sebagai berikut
Baca Juga: Membuat Custom Validation di Laravel 5.6
Membuat Cart Dengan Cookie
Menentukan schema yang akan digunakan dalam membuat cart atau keranjang pesanan ini memiliki pilihan, diantaranya: menggunakan table bantuan ke database atau dengan menggunakan cookie yang disimpan sementara dibrowser. Lalu, bagaimana sih menentukan schema yang sebaiknya digunakan? Jika kita analisa, aplikasi yang akan kita capai, dimana proses transaksinya dilakukan oleh kasir yang sedang bertugas dan diselesaikan saat itu juga, maka pilihan yang tepat untuk digunakan adalah memanfaatkan cookie.
Hal yang diinginkan adalah ketika tombol untuk menambahkan ke keranjang tersebut ditekan, maka produk yang dipilih beserta qty nya akan disimpan ke dalam cookie. Buka file resources/views/orders/add.blade.php
, kemudian modifikasi menjadi:
@extends('layouts.master')
@section('title')
<title>Transaksi</title>
@endsection
@section('css')
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" />
@endsection
@section('content')
<div class="content-wrapper">
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">Transaksi</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active">Transaksi</li>
</ol>
</div>
</div>
</div>
</div>
<section class="content" id="dw">
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
@card
@slot('title')
@endslot
<div class="row">
<div class="col-md-4">
<!-- SUBMIT DIJALANKAN KETIKA TOMBOL DITEKAN -->
<form action="#" @submit.prevent="addToCart" method="post">
<div class="form-group">
<label for="">Produk</label>
<select name="product_id" id="product_id"
v-model="cart.product_id"
class="form-control" required width="100%">
<option value="">Pilih</option>
@foreach ($products as $product)
<option value="{{ $product->id }}">{{ $product->code }} - {{ $product->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="">Qty</label>
<input type="number" name="qty"
v-model="cart.qty"
id="qty" value="1"
min="1" class="form-control">
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm"
:disabled="submitCart"
>
<i class="fa fa-shopping-cart"></i> @{{ submitCart ? 'Loading...':'Ke Keranjang' }}
</button>
</div>
</form>
</div>
<!-- MENAMPILKAN DETAIL PRODUCT -->
<div class="col-md-5">
<h4>Detail Produk</h4>
<div v-if="product.name">
<table class="table table-stripped">
<tr>
<th>Kode</th>
<td>:</td>
<td>@{{ product.code }}</td>
</tr>
<tr>
<th width="3%">Produk</th>
<td width="2%">:</td>
<td>@{{ product.name }}</td>
</tr>
<tr>
<th>Harga</th>
<td>:</td>
<td>@{{ product.price | currency }}</td>
</tr>
</table>
</div>
</div>
<!-- MENAMPILKAN IMAGE DARI PRODUCT -->
<div class="col-md-3" v-if="product.photo">
<img :src="'/uploads/product/' + product.photo"
height="150px"
width="150px"
:alt="product.name">
</div>
</div>
@slot('footer')
@endslot
@endcard
</div>
<!-- MENAMPILKAN LIST PRODUCT YANG ADA DI KERANJANG -->
<div class="col-md-4">
@card
@slot('title')
Keranjang
@endslot
<table class="table table-hover">
<thead>
<tr>
<th>Produk</th>
<th>Harga</th>
<th>Qty</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<!-- MENGGUNAKAN LOOPING VUEJS -->
<tr v-for="(row, index) in shoppingCart">
<td>@{{ row.name }} (@{{ row.code }})</td>
<td>@{{ row.price | currency }}</td>
<td>@{{ row.qty }}</td>
<td>
<!-- EVENT ONCLICK UNTUK MENGHAPUS CART -->
<button
@click.prevent="removeCart(index)"
class="btn btn-danger btn-sm">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
@slot('footer')
<div class="card-footer text-muted">
<a href="{{ route('order.checkout') }}"
class="btn btn-info btn-sm float-right">
Checkout
</a>
</div>
@endslot
@endcard
</div>
</div>
</div>
</section>
</div>
@endsection
@section('js')
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/accounting.js/0.4.1/accounting.min.js"></script>
<script src="{{ asset('js/transaksi.js') }}"></script>
@endsection
Penjelasan: Perubahan yang mencolok yang dilakukan diatas adalah adanya penambahan card untuk menampilkan list cart. Hal lainnya adalah dengan memberikan event on submit / @submit
. Penjelasan lainnya sudah saya sematkan di dalam coding.
Karena terdapat beberapa penambahan method addToCart
, removeCart
dan variable shoppingCart
berupa array, juga cart.product_id
dan cart.qty
, maka kita perlu melakukan perubahan pada file transaksi.js
, buka kembali file tersebut yang terletak di resources/assets/js/transaksi.js
, kemudian modifikasi menjadi:
import Vue from 'vue'
import axios from 'axios'
//import sweetalert library
import VueSweetalert2 from 'vue-sweetalert2';
Vue.filter('currency', function (money) {
return accounting.formatMoney(money, "Rp ", 2, ".", ",")
})
//use sweetalert
Vue.use(VueSweetalert2);
new Vue({
el: '#dw',
data: {
product: {
id: '',
price: '',
name: '',
photo: ''
},
//menambahkan cart
cart: {
product_id: '',
qty: 1
},
//untuk menampung list cart
shoppingCart: [],
submitCart: false
},
watch: {
//apabila nilai dari product > id berubah maka
'product.id': function() {
//mengecek jika nilai dari product > id ada
if (this.product.id) {
//maka akan menjalankan methods getProduct
this.getProduct()
}
}
},
//menggunakan library select2 ketika file ini di-load
mounted() {
$('#product_id').select2({
width: '100%'
}).on('change', () => {
//apabila terjadi perubahan nilai yg dipilih maka nilai tersebut
//akan disimpan di dalam var product > id
this.product.id = $('#product_id').val();
});
//memanggil method getCart() untuk me-load cookie cart
this.getCart()
},
methods: {
getProduct() {
//fetch ke server menggunakan axios dengan mengirimkan parameter id
//dengan url /api/product/{id}
axios.get(`/api/product/${this.product.id}`)
.then((response) => {
//assign data yang diterima dari server ke var product
this.product = response.data
})
},
//method untuk menambahkan product yang dipilih ke dalam cart
addToCart() {
this.submitCart = true;
//send data ke server
axios.post('/api/cart', this.cart)
.then((response) => {
setTimeout(() => {
//apabila berhasil, data disimpan ke dalam var shoppingCart
this.shoppingCart = response.data
//mengosongkan var
this.cart.product_id = ''
this.cart.qty = 1
this.product = {
id: '',
price: '',
name: '',
photo: ''
}
$('#product_id').val('')
this.submitCart = false
}, 2000)
})
.catch((error) => {
})
},
//mengambil list cart yang telah disimpan
getCart() {
//fetch data ke server
axios.get('/api/cart')
.then((response) => {
//data yang diterima disimpan ke dalam var shoppingCart
this.shoppingCart = response.data
})
},
//menghapus cart
removeCart(id) {
//menampilkan konfirmasi dengan sweetalert
this.$swal({
title: 'Kamu Yakin?',
text: 'Kamu Tidak Dapat Mengembalikan Tindakan Ini!',
type: 'warning',
showCancelButton: true,
confirmButtonText: 'Iya, Lanjutkan!',
cancelButtonText: 'Tidak, Batalkan!',
showCloseButton: true,
showLoaderOnConfirm: true,
preConfirm: () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 2000)
})
},
allowOutsideClick: () => !this.$swal.isLoading()
}).then ((result) => {
//apabila disetujui
if (result.value) {
//kirim data ke server
axios.delete(`/api/cart/${id}`)
.then ((response) => {
//load cart yang baru
this.getCart();
})
.catch ((error) => {
console.log(error);
})
}
})
}
}
})
Penjelasan: Yang perlu menjadi catatan adalah uri: /api/cart
dengan method POST, /api/cart
dengan method GET, dan /api/cart/{id}
dengan method DELETE. Ketiga uri ini perlu kita handle dari sisi server side untuk mem-proses permintaan yang dikirimkan.
Buka file OrderController.php
, kemudian tambahkan method:
public function addToCart(Request $request)
{
//validasi data yang diterima
//dari ajax request addToCart mengirimkan product_id dan qty
$this->validate($request, [
'product_id' => 'required|exists:products,id',
'qty' => 'required|integer'
]);
//mengambil data product berdasarkan id
$product = Product::findOrFail($request->product_id);
//mengambil cookie cart dengan $request->cookie('cart')
$getCart = json_decode($request->cookie('cart'), true);
//jika datanya ada
if ($getCart) {
//jika key nya exists berdasarkan product_id
if (array_key_exists($request->product_id, $getCart)) {
//jumlahkan qty barangnya
$getCart[$request->product_id]['qty'] += $request->qty;
//dikirim kembali untuk disimpan ke cookie
return response()->json($getCart, 200)
->cookie('cart', json_encode($getCart), 120);
}
}
//jika cart kosong, maka tambahkan cart baru
$getCart[$request->product_id] = [
'code' => $product->code,
'name' => $product->name,
'price' => $product->price,
'qty' => $request->qty
];
//kirim responsenya kemudian simpan ke cookie
return response()->json($getCart, 200)
->cookie('cart', json_encode($getCart), 120);
}
public function getCart()
{
//mengambil cart dari cookie
$cart = json_decode(request()->cookie('cart'), true);
//mengirimkan kembali dalam bentuk json untuk ditampilkan dengan vuejs
return response()->json($cart, 200);
}
public function removeCart($id)
{
$cart = json_decode(request()->cookie('cart'), true);
//menghapus cart berdasarkan product_id
unset($cart[$id]);
//cart diperbaharui
return response()->json($cart, 200)->cookie('cart', json_encode($cart), 120);
}
Note: 120
adalah lama data tersebut akan tersimpan di dalam cookie, dengan satuan menit.
Laravel pada versi yang terbaru telah meng-enkripsi cookie yang disimpan, sehingga tidak dapat dimodifikasi ataupun dibaca oleh client. So, untuk melewati proses enkripsi ini, buka file Middleware EncryptCookies.php
, kemudian modifikasi menjadi:
protected $except = [
'cart'
];
Note: cart merupakan nama dari cookie yang disimpan.
Buka file routes/api.php
kemudian tambahkan code berikut untuk membuat uri api yang akan digunakan oleh Vue dalam melakukan fetch data ke server:
Route::post('/cart', 'OrderController@addToCart');
Route::get('/cart', 'OrderController@getCart');
Route::delete('/cart/{id}', 'OrderController@removeCart');
Karena terjadi perubahan dalam file transaksi.js
, maka jalankan kembali command:
npm run dev
Adapun tampilan ketika terdapat product di dalam cart:
Dan ketika akan menghapus product dari cart, maka akan memunculkan dialog konfirmasi
Kesimpulan
Tidak bermaksud mengakhirinya tanpa kepastian *eh, akan tetapi artikel ini sudah cukup panjang sehingga untuk bagian transaksi akan kami bagi menjadi beberapa bagian. Sepanjang artikel ini kita telah belajar bagaimana melakukan fetch data menggunakan ajax request, juga bagaimana menyimpan data sementara ke dalam cookie dan bagaimana menghapus cookie. Hal lain yang kita dapatkan adalah bagaimana berinteraksi antara Vue.sj sebagaia client side dan Laravel di server side.
Untuk dokumentasinya dapat kamu lihat di Github.
Comments