(Part 7) Membuat Aplikasi POS (Point of Sales) Laravel 5.6 - Transaksi (Bagian 2)

(Part 7) Membuat Aplikasi POS (Point of Sales) Laravel 5.6 - Transaksi (Bagian 2)

Pendahuluan

Tahapan ini merupakan tahapan kedua dari module transaksi dalam aplikasi Point of Sales yang sedang kita buat, dimana sebelumnya, kita telah membuat keranjang belanja yang akan menampung list barang yang akan diteruskan untuk didata sebagai sebuah transaksi.

Dari data transaksi yang telah disimpan, selanjutkan akan kita olah sesuai dengan keinginan untuk menunjang informasi yang akan didapatkan penggunanya, sehingga detail transaksi perlu disimpan dengan sangat rapi, tidak terkait dan tidak dapat dipengaruhi oleh informasi dari data barang sebelumnya. So, apabila terjadi perubahan baik dari segi harga ataupun informasi lainnya dari table sebelumnya, maka data yag tersimpan di dalam table yang meng-handle transaksi tidak akan terpengaruh.

Baca Juga: Membuat Aplikasi POS (Point of Sales) laravel 5.6 - Transaksi (Cart)

Reusable View Cart

Sebelum melanjutkan materi yang telah dibahas, tahap awal yang akan dilakukan adalah membuat reusable view khusus untuk menampilkan keranjang, sehingga code tersebut tidak berulang kita ketikkan namun memiliki fungsi yang sama. Keuntungan lainnya adalah kita lebih mudah dalam me-maintenace kedepannya. Schema-nya adalah tampilan dari list keranjang dimiliki akan ditampilkan pada dua halaman yakni halaman transaksi dan halaman checkout. Sehingga list keranjang tersebut akan dipindahkan ke dalam satu file tersendiri agar dapat dipanggil berulang kali.

Buat file cart.blade.php di dalam folder resources/views/orders, kemudian pindahkan code untuk menampilkan cart, sehingga akan tampak seperti ini:

<div class="col-md-4">
    @card
        @slot('title')
        Keranjang
        @endslot
​
        <div class="table-responsive">
            <table class="table table-hover">
                <thead>
                    <tr>
                        <th>Produk</th>
                        <th>Harga</th>
                        <th>Qty</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="(row, index) in shoppingCart">
                        <td>@{{ row.name }} (@{{ row.code }})</td>
                        <td>@{{ row.price | currency }}</td>
                        <td>@{{ row.qty }}</td>
                        <td>
                            <button 
                                @click.prevent="removeCart(index)"    
                                class="btn btn-danger btn-sm">
                                <i class="fa fa-trash"></i>
                            </button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
        @slot('footer')
        <div class="card-footer text-muted">
            @if (url()->current() == route('order.transaksi'))
            <a href="{{ route('order.checkout') }}" 
                class="btn btn-info btn-sm float-right">
                Checkout
            </a>
            @else
            <a href="{{ route('order.transaksi') }}"
                class="btn btn-secondary btn-sm float-right"
                >
                Kembali
            </a>
            @endif
        </div>
        @endslot
    @endcard
</div>

Penjelasan: Terdapat sedikit perubahan, yakni menambahkan logika if, jika URI yang sedang aktif saat ini sama dengan route order.transaksi, maka yang akan ditampilkan adalah tombol Checkout, selain itu adalah tombol kembali ke form order.

Selanjutnya buka file resources/views/orders/add.blade.php, kemudian hapus kodingan untuk menampilkan list cart dan replace dengan @include('orders.cart'), jika dilihat secara lengkap, isi dari file add.blade.php akan menjadi seperti ini:

@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">
                                    <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>
                                <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>
                                <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>
                    @include('orders.cart')
                </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: Jadi, kita hanya perlu meng-include file cart.blade.php yang dibuat sebelumnya tanpa perlu menuliskan kembali kodingannya.

Create Form Checkout

Pendekatan terakhir sebelum data dari transaksi tersebut disimpan ke dalam database adalah dengan membuat form checkout untuk mengambil data pelanggan, dengan tujuan agar dapat diketahui siapa pemilik transaksi tersebut. Buka file OrderController.php, kemudian tambahkan method:

public function checkout()
{
    return view('orders.checkout');
}

Buat file checkout.blade.php di dalam folder resources/views/orders, kemudian tambahkan code berikut:

@extends('layouts.master')
​
@section('title')
    <title>Checkout</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">Checkout</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"><a href="{{ route('order.transaksi') }}">Transaksi</a></li>
                            <li class="breadcrumb-item active">Checkout</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')
                                <h4 class="card-title">Data Pelanggan</h4>
                            @endslot
                                <!-- JIKA VALUE DARI message ada, maka alert success akan ditampilkan -->
                                <div v-if="message" class="alert alert-success">
                                    Transaksi telah disimpan, Invoice: <strong>#@{{ message }}</strong>
                                </div>
                                <div class="form-group">
                                    <label for="">Email</label>
                                    <input type="text" name="email" 
                                        v-model="customer.email"
                                        class="form-control" 
                                        @keyup.enter.prevent="searchCustomer"
                                        required
                                        >
                                    <p>Tekan enter untuk mengecek email.</p>
                                    <!-- EVENT KETIKA TOMBOL ENTER DITEKAN, MAKA AKAN MEMANGGIL METHOD searchCustomer dari Vuejs -->
                                </div>
                        
                                <!-- JIKA formCustomer BERNILAI TRUE, MAKA FORM AKAN DITAMPILKAN -->
                                <div v-if="formCustomer">
                                    <div class="form-group">
                                        <label for="">Nama Pelanggan</label>
                                        <input type="text" name="name" 
                                            v-model="customer.name"
                                            :disabled="resultStatus"
                                            class="form-control" required>
                                    </div>
                                    <div class="form-group">
                                        <label for="">Alamat</label>
                                        <textarea name="address"
                                            class="form-control"
                                            :disabled="resultStatus"
                                            v-model="customer.address"
                                            cols="5" rows="5" required></textarea>
                                    </div>
                                    <div class="form-group">
                                        <label for="">No Telp</label>
                                        <input type="text" name="phone" 
                                            v-model="customer.phone"
                                            :disabled="resultStatus"
                                            class="form-control" required>
                                    </div>
                                </div>
                            @slot('footer')
                                <div class="card-footer text-muted">
                                    <!-- JIKA VALUE DARI errorMessage ada, maka alert danger akan ditampilkan -->
                                    <div v-if="errorMessage" class="alert alert-danger">
                                        @{{ errorMessage }}
                                    </div>
                                    <!-- JIKA TOMBOL DITEKAN MAKA AKAN MEMANGGIL METHOD sendOrder -->
                                    <button class="btn btn-primary btn-sm float-right"
                                        :disabled="submitForm"  
                                        @click.prevent="sendOrder"
                                        >
                                        @{{ submitForm ? 'Loading...':'Order Now' }}
                                    </button>
                                </div>
                            @endslot
                        @endcard
                    </div>
                    @include('orders.cart')
                </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: Sebagian penjelasan telah disematkan ke dalam coding, maka yang perlu dijelaskan adalah bahwa schema yang akan digunakan adalah form untuk meng-input data customer bersifat dinamis, dimana ketika email tersebut sudah ada di dalam database, maka selanjutnya akan menampilkan detail dari customer terkait, jika tidak, maka akan menampilkan input-an lainnya untuk melengkapi data customer tersebut.

Untuk meng-handle js-nya, kita masih akan menggunakan file transaksi.js, buka file tersebut yang terletak di dalam folder resources/assets/js, kemudian tambahkan bagian ini ke dalam property data:

customer: {
    email: ''
},
formCustomer: false,
resultStatus: false,
submitForm: false,
errorMessage: '',
message: ''

Penjelasan: formCustomer untuk meng-handle form, dimana ketika bernilai true maka form yang diapit akan ditampilkan. resultStatus untuk meng-handle property dari form input, dimana ketika bernilai true maka input-an tersebut akan disabled. submitForm akan meng-handle button Order Now, dimana ketika bernilai true maka button tersebut disabled dan value-nya berubah menjadi loading. Terakhir, customer{}, object tersebut akan menyimpan value dari form input-an yang terkait untuk data binding.

Kemudian didalam property watch, tambahkan code berikut:

'customer.email': function() {
    this.formCustomer = false
    if (this.customer.name != '') {
        this.customer = {
            name: '',
            phone: '',
            address: ''
        }
    }
}

Penjelasan: Apabila nilai dari customer.email berubah, maka formCustomer akan di-set menjadi false, dan customer{} akan di-set menjadi kosong.

Lalu tambahkan methods berikut ke dalam property methods:

searchCustomer() {
    axios.post('/api/customer/search', {
        email: this.customer.email
    })
    .then((response) => {
        if (response.data.status == 'success') {
            this.customer = response.data.data
            this.resultStatus = true
        } 
        this.formCustomer = true
    })
    .catch((error) => {
​
    })
},
// method sendOrder() kita biarkan kosong terlebih dahulu, section selanjutnya akan di modifikasi
sendOrder() {
​
}

Penjelasan: Ketika dalam input-an email ditekan enter, maka methods ini akan berjalan, dimana fungsinya akan mengirimkan parameter email untuk di-cek ke database. Apabila email tersebut exists, maka customer akan di-replace dengan data yang diterima dari database dan resultStatus akan di-set menjadi true sehingga form input-an selain email akan ditambahkan property disabled. Terakhir, formCustomer akan di-set menjadi true, sehingga input-an tersebut ditampilkan.

Karena terdapat request ke /api/customer/search, maka kita perlu meng-handle request tersebut terlebih dahulu. Buat controller dengan command:

php artisan make:controller CustomerController

Buka file CustomerController.php, kemudian tambahkan method berikut:

public function search(Request $request)
{
    $this->validate($request, [
        'email' => 'required|email'
    ]);
​
    $customer = Customer::where('email', $request->email)->first();
    if ($customer) {
        return response()->json([
            'status' => 'success',
            'data' => $customer
        ], 200);
    }
    return response()->json([
        'status' => 'failed',
        'data' => []
    ]);
}

Penjelasan: Line 3-5, validasi request yang diterima harus berupa email. Line-7, mengambil records berdasarkan email yang dikirim dari form. Line 8-13, mengecek jika exists maka me-return data tersebut dengan status success. Line-14-17, akan me-return data kosong dengan status failed jika data customer tidak tersedia.

Jangan lupa tambahkan use statement:

use App\Customer;

Buka file routes/api.php, kemudian tambahkan:

Route::post('/customer/search', 'CustomerController@search');

Kemudian buka file routes/web.php, kemudian tambahkan route berikut di dalam group yang menggunakan role kasir:

Route::group(['middleware' => ['role:kasir']], function() {
    ...

 
    Route::get('/checkout', 'OrderController@checkout')->name('order.checkout');
});

Terakhir, jangan lupa jalankan command berikut agar file js yang telah diperbaharui di-generate kembali.

npm run dev

Maka tampilan yang akan kita peroleh akan tampak seperti berikut

pos laravel transaksi

Setelah mengisi email, dan belum exists akan tampak seperti berikut

pos laravel transaksi

Baca Juga: Membuat Laporan Laravel Excel 3.0

Handle Transaction

Langkah terakhir adalah bergeser ke backend, meng-handle transaction yang dikirim dari form untuk kemudian disimpan ke dalam masing-masing table yang terkait. Apabila kita menekan tombol Order Now, tidak akan terjadi apa-apa karena method yang meng-handle-nya masih dibiarkan kosong. Sekarang, tugas kita adalah memodifikasi method tersebut, buka file transaksi.js yang terletak di dalam folder resources/views/assets/js, kemudian modifikasi method sendOrder() sehingga menjadi:

sendOrder() {
    //Mengosongkan var errorMessage dan message
    this.errorMessage = ''
    this.message = ''
    
    //jika var customer.email dan kawan-kawannya tidak kosong
    if (this.customer.email != '' && this.customer.name != '' && this.customer.phone != '' && this.customer.address != '') {
        //maka akan menampilkan kotak dialog konfirmasi
        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) => {
            //jika di setujui
            if (result.value) {
                //maka submitForm akan di-set menjadi true sehingga menciptakan efek loading
                this.submitForm = true
                //mengirimkan data dengan uri /checkout
                axios.post('/checkout', this.customer)
                .then((response) => {
                    setTimeout(() => {
                        //jika responsenya berhasil, maka cart di-reload
                        this.getCart();
                        //message di-set untuk ditampilkan
                        this.message = response.data.message
                        //form customer dikosongkan
                        this.customer = {
                            name: '',
                            phone: '',
                            address: ''
                        }
                        //submitForm kembali di-set menjadi false
                        this.submitForm = false
                    }, 1000)
                })
                .catch((error) => {
                    console.log(error)
                })
            }
        })
    } else {
        //jika form kosong, maka error message ditampilkan
        this.errorMessage = 'Masih ada inputan yang kosong!'
    }
}

Penjelasan: Yang mis dari penjelasan yang disematkan kedalam coding adalah uri /checkout tidak menggunakan prefix API. Kenapa demikian? karena kita belum mencapai materi untuk membuat API yang menggunakan token sehingga dapat melakukan pengecekan authentication, sedangkan di dalam table orders membutuhkan user_id yang sedang login. Sehingga untuk sementara kita simpan route-nya terlebih dahulu kedalam web.php.

Buka file OrderController.php, kemudian tambahkan method berikut:

public function storeOrder(Request $request)
{
    //validasi 
    $this->validate($request, [
        'email' => 'required|email',
        'name' => 'required|string|max:100',
        'address' => 'required',
        'phone' => 'required|numeric'
    ]);

 
    //mengambil list cart dari cookie
    $cart = json_decode($request->cookie('cart'), true);
    //memanipulasi array untuk menciptakan key baru yakni result dari hasil perkalian price * qty
    $result = collect($cart)->map(function($value) {
        return [
            'code' => $value['code'],
            'name' => $value['name'],
            'qty' => $value['qty'],
            'price' => $value['price'],
            'result' => $value['price'] * $value['qty']
        ];
    })->all();

 
    //database transaction
    DB::beginTransaction();
    try {
        //menyimpan data ke table customers
        $customer = Customer::firstOrCreate([
            'email' => $request->email
        ], [
            'name' => $request->name,
            'address' => $request->address,
            'phone' => $request->phone
        ]);

 
        //menyimpan data ke table orders
        $order = Order::create([
            'invoice' => $this->generateInvoice(),
            'customer_id' => $customer->id,
            'user_id' => auth()->user()->id,
            'total' => array_sum(array_column($result, 'result'))
            //array_sum untuk menjumlahkan value dari result
        ]);

 
        //looping cart untuk disimpan ke table order_details
        foreach ($result as $key => $row) {
            $order->order_detail()->create([
                'product_id' => $key,
                'qty' => $row['qty'],
                'price' => $row['price']
            ]);
        }
        //apabila tidak terjadi error, penyimpanan diverifikasi
        DB::commit();

 
        //me-return status dan message berupa code invoice, dan menghapus cookie
        return response()->json([
            'status' => 'success',
            'message' => $order->invoice,
        ], 200)->cookie(Cookie::forget('cart'));
    } catch (\Exception $e) {
        //jika ada error, maka akan dirollback sehingga tidak ada data yang tersimpan 
        DB::rollback();
        //pesan gagal akan di-return
        return response()->json([
            'status' => 'failed',
            'message' => $e->getMessage()
        ], 400);
    }
}

Note: Database transaction digunakan untuk memastikan tidak ada masalah saat melakukan penyimpanan ke multiple table, sehingga keakuratan data dapat dijaga. Apabila tidak menggunakan database transaction, jia terjadi error ditengah penyimpanan data, maka data di-table sebelumnya tidak akan direset, sehingga data yang tersimpan di table sebelumnya tetap akan dipertahankan. Padahal terjadi error, yang harusnya data tersebut dihilangkan seperti sediakala sebelum transaksi dilakukan.

Karena kita memanggil method $this->generateInvoice(), sehingga method tersebut perlu dibuat juga untuk meng-generate invoice. Masih di dalam file yang sama tambahkan code berikut:

public function generateInvoice()
{
    //mengambil data dari table orders
    $order = Order::orderBy('created_at', 'DESC');
    //jika sudah terdapat records
    if ($order->count() > 0) {
        //mengambil data pertama yang sdh dishort DESC
        $order = $order->first();
        //explode invoice untuk mendapatkan angkanya
        $explode = explode('-', $order->invoice);
        //angka dari hasil explode di +1
        return 'INV-' . $explode[1] + 1;
    }
    //jika belum terdapat records maka akan me-return INV-1
    return 'INV-1';
}

Jangan lupa untuk menambahkan use statement:

use App\Order;
use App\Order_detail;
use Cookie;
use DB;

Buka file routes/web.php, kemudian tambahkan route berikut di dalam role kasir:

Route::post('/checkout', 'OrderController@storeOrder')->name('order.storeOrder');

Dari ketiga model diatas, kita menggunakan mass assigment untuk menyimpan data, maka perlu menambahkan fillable atau guarded agar mendapatkan izin untuk memasukkan data ke masing-masing table. Buka file Customer.php, kemudian tambahkan code:

protected $guarded = [];

Buka file Order.php, kemudian tambahkan code:

protected $guarded = [];
​
//Model relationships ke Order_detail menggunakan hasMany
public function order_detail()
{
    return $this->hasMany(Order_detail::class);
}

Buka file Order_detail.php, kemudian tambahkan code:

protected $guarded = [];
​
//Model relationships ke Order
public function order()
{
    return $this->belongsTo(Order::class);
}

Terakhir, jalankan kembali command berikut:

npm run dev

Kesimpulan

Sepanjang artikel ini kita telah belajar bagaimana membuat re-usable view, membuat dynamic form dan terakhir meng-handle transaction dengan menggunakan database transaction. Hal ini tentu saja bukan akhir dari materi membuat aplikasi POS (Point of Sales), karena masih banyak pembenahan yang akan kita lakukan kedepannya, termasuk menambahkan module untuk mengelola transaksi dan juga laporan.

Ohya apabila kamu tertinggal dan sulit merangkai potongan code diatas, silahkan di baca dengan seksama atau jangan sungkan untuk meninggalkan komentar. Dokumentasi code dapat kamu lihat di Github.

Category:
Share:

Comments