(Part 6) Membuat Aplikasi POS (Point of Sales) Laravel 5.6 - Transaksi (Cart)

(Part 6) Membuat Aplikasi POS (Point of Sales) Laravel 5.6 - Transaksi (Cart)

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

pos laravel

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:

pos laravel

Dan ketika akan menghapus product dari cart, maka akan memunculkan dialog konfirmasi

pos laravel

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.

Category:
Share:

Comments