Tutorial Laravel Livewire - #10 - Membuat Module POS Dengan Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah module pos dengan livewire.

Rafi Taufiqurrahman
Dipublish 09/07/2024

Pendahuluan

Setelah berhasil membuat module category dan module product, disini kita akan lanjutkan untuk pembuatan module pos. Module ini kita gunakan sebagai module transaksi pada study case kita kali ini.

Component Pos Index

Silahkan teman-teman buka terminal-nya, kemudian jalankan perintah berikut ini :

Terminal
php artisan make:livewire pages.pos.index

Setelah berhasil menjalankan perintah diatas, kita akan mendapatkan 2 buah file baru yang terletak di :

  1. app/Livewire/Pages/Pos/Index.php
  2. resources/views/livewire/pages/pos/index.blade.php

Silahkan teman-teman buka file App/Livewire/Pages/Pos/Index.php, kemudian tambahkan kode berikut ini :

Pos/Index.php
<?php

namespace App\Livewire\Pages\Pos;

use App\Models\Cart;
use App\Models\Product;
use Livewire\Component;
use App\Models\Transaction;
use Livewire\Attributes\Url;
use Livewire\WithPagination;
use Livewire\Attributes\Title;
use Livewire\Attributes\Layout;
use Illuminate\Support\Facades\DB;

class Index extends Component
{
    use WithPagination;

    // define layout
    #[Layout('layouts.app')]
    // define title
    #[Title('Pos')]

    // define property
    #[Url]
    public $search;
    public $carts;
    public $qty;
    public $totalQty;
    public $subTotalPrice;
    public $totalPrice;
    public $pay;
    public $change;

    // define method userCarts
    public function userCarts()
    {
        // get carts data by userId
        $this->carts = Cart::whereUserId(auth()->user()->id)->get();
    }

    // define lifecycle hooks
    public function mount()
    {
        // call method userCarts
        $this->userCarts();

        // loop carts data
        foreach($this->carts as $cart){
            // set totalQty
            $this->totalQty[$cart->id] = $cart->quantity;
            // set subTotalPrice
            $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
            // set totalPrice
            $this->totalPrice = $this->totalPrice + $this->subTotalPrice[$cart->id];
            // set change
            $this->change = 0;
        }
    }

    // define method addToCart
    public function addToCart($id)
    {
        // get product item by id
        $product = Product::findOrFail($id);

        // get cart items by product id and user id
        $cart = Cart::whereProductId($product->id)->whereUserId(auth()->user()->id)->first();

        // do it when cart is true
        if($cart && $product->quantity > 0){
            // do it when cart qty is less than product qty
            if($cart->quantity < $product->quantity)
                // update cart qty
                $cart->update([
                    'quantity' => $cart->quantity + 1,
                ]);
        }else{
             // do it when product quantity is more than 0
            if($product->quantity > 0)
                // create new cart data
                Cart::create([
                    'user_id' => auth()->user()->id,
                    'product_id' => $product->id,
                    'quantity' => 1,
                    'price' => $product->price,
                ]);
        }

        // render view
        return $this->redirect('/pos', navigate: true);
    }

    // define method incrementQty
    public function incrementQty($id)
    {
        // get cart items by id
        $cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

        // get products by id
        $product = Product::findOrFail($cart->product_id);

        // do it when cart qty is less than product qty
        if($cart->quantity < $product->quantity){
            // update cart qty
            $cart->increment('quantity');
            // update totalQty
            $this->totalQty[$cart->id] = $cart->quantity;
            // update subTotalPrice
            $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
            // update totalPrice
            $this->totalPrice += $cart->price;
            // update change
            $this->change = $this->totalPrice;
        }
    }

    // define method decrementQty
    public function decrementQty($id)
    {
        // get cart items by id
        $cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

        // do it when cart qty more than 1
        if($cart->quantity > 1){
            // update cart qty
            $cart->decrement('quantity');
            // update totalQty
            $this->totalQty[$cart->id] = $cart->quantity;
            // update subTotalPrice
            $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
            // update totalPrice
            $this->totalPrice -= $cart->price;
            // update change
            $this->change = $this->totalPrice;
        }
    }

    // define method removeItem
    public function removeItem($id)
    {
        // get cart data by id
        $cart = Cart::findOrFail($id);

        // delete cart data
        $cart->delete();

        // render view
        return $this->redirect('/pos', navigate: true);
    }

    // define method updateChange
    public function updateChange()
    {
        // do it when property pay is more than 0
        if($this->pay > 0 )
            // calculate change
            $this->change = $this->pay - $this->totalPrice;
        else
            // change is totalPrice
            $this->change = $this->totalPrice;
    }

    // define lifecycle hook
    public function updatedPay()
    {
        // call method updateChange
        $this->updateChange();
    }

    // define method save
    public function save()
    {
        // start db transaction
        DB::transaction(function(){
            // loop
            for($i = 0; $i < 6; $i++)
                // generate random char
                $char = rand(0,1) ? rand(0, 9) : chr(rand(ord('a'), ord('z')));

            // generate invoice
            $invoice = 'TRX-'.str()->upper($char);

            // create new transaction
            $transaction = Transaction::create([
                'invoice' => $invoice,
                'user_id' => auth()->user()->id,
                'cash' => $this->pay,
                'grand_total' => $this->totalPrice,
                'change' => $this->change,
            ]);

            // loop user carts
            foreach($this->carts as $cart){
                // create new transaction details
                $transaction->details()->create([
                    'product_id' => $cart->product_id,
                    'quantity' => $cart->quantity,
                    'price' => $cart->price
                ]);

                // update qty product
                Product::where('id', $cart->product_id)->decrement('quantity', $cart->quantity);
            }

            // delete cart
            $this->carts->each->delete();

            // render view
            return $this->redirect('/dashboard', navigate: true);
        });
    }

    public function render()
    {
        // list products data
        $products = Product::query()
            ->with('category')
            ->when($this->search, function($query){
                $query->where('name', 'like', '%'. $this->search . '%');
            })->paginate(9);

        // render view
        return view('livewire.pages.pos.index', compact('products'));
    }
}

Pada kode diatas pertama - tama, kita menggunakan sebuah trait yang telah disedikan oleh livewire.

Products/Index.php
use WithPagination;

Selanjutnya kita mendefinisikan sebuah attribute title dan layout.

Products/Index.php
// define layout
#[Layout('layouts.app')]
// define title
#[Title('Pos')]

Kemudian kita juga mendefinisikan beberapa property diantaranya search, carts, qty, totalQty, subTotalPrice, totalPrice, pay dan change.

Products/Index.php
// define property
#[Url]
public $search;
public $carts;
public $qty;
public $totalQty;
public $subTotalPrice;
public $totalPrice;
public $pay;
public $change;

Method userCarts

Method ini kita gunakan untuk menampilakan data carts sesuai dengan user yang sedang login saat ini.

Pos/Index.php
// define method userCarts
public function userCarts()
{
    // get carts data by userId
    $this->carts = Cart::whereUserId(auth()->user()->id)->get();
}

Pada kode diatas, kita assign property carts dengan data cart yang dimiliki oleh user yang sedang login.

Method Mount

Method ini merupakan lifecycle hook yang disediakan oleh livewire, method ini akan dijalankan pertama kali ketika component di render.

Pos/Index.php
// define lifecycle hooks
public function mount()
{
    // call method userCarts
    $this->userCarts();

    // loop carts data
    foreach($this->carts as $cart){
        // set totalQty
        $this->totalQty[$cart->id] = $cart->quantity;
        // set subTotalPrice
        $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
        // set totalPrice
        $this->totalPrice = $this->totalPrice + $this->subTotalPrice[$cart->id];
        // set change
        $this->change = 0;
    }
}

Pada method ini, pertama kita memanggil sebuah method userCarts yang telah kita definisikan sebelumnya.

Pos/Index.php
// call method userCarts
$this->userCarts();

Kemudian kita lanjutkan untuk melakukan assign beberapa property yang telah kita definisikan dari hasil perulangan data carts.

Pos/Index.php
// loop carts data
foreach($this->carts as $cart){
    // set totalQty
    $this->totalQty[$cart->id] = $cart->quantity;
    // set subTotalPrice
    $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
    // set totalPrice
    $this->totalPrice = $this->totalPrice + $this->subTotalPrice[$cart->id];
    // set change
    $this->change = 0;
}

Method addToCart

Method ini kita gunakan sebagai penambahan data kedalam tabel carts, sesuai dengan parameter id yang dikirimkan.

Pos/Index.php
// define method addToCart
public function addToCart($id)
{
    // get product item by id
    $product = Product::findOrFail($id);

    // get cart items by product id and user id
    $cart = Cart::whereProductId($product->id)->whereUserId(auth()->user()->id)->first();

    // do it when cart is true
    if($cart && $product->quantity > 0){
        // do it when cart qty is less than product qty
        if($cart->quantity < $product->quantity)
            // update cart qty
            $cart->update([
                'quantity' => $cart->quantity + 1,
            ]);
    }else{
         // do it when product quantity is more than 0
        if($product->quantity > 0)
            // create new cart data
            Cart::create([
                'user_id' => auth()->user()->id,
                'product_id' => $product->id,
                'quantity' => 1,
                'price' => $product->price,
            ]);
    }

    // render view
    return $this->redirect('/pos', navigate: true);
}

Pertama - tama, kita lakukan pencarian data product yang sesuai dengan parmeter id.

Pos/Index.php
// get product item by id
$product = Product::findOrFail($id);

Selanjutnya kita lakukan pencarian data cart yang sesuai dengan product yang telah kita temukan dan berdasarkan user yang sedang login saat ini.

Pos/Index.php
// get cart items by product id and user id
$cart = Cart::whereProductId($product->id)->whereUserId(auth()->user()->id)->first();

Berikutnya kita lakukan pengecekan data cart, jika nilainya true maka kita akan lakukan update quantity saja.

Pos/Index.php
// do it when cart is true
if($cart){
    // do it when cart qty is less than product qty
    if($cart->quantity < $product->quantity)
        // update cart qty
        $cart->update([
            'quantity' => $cart->quantity + 1,
        ]);
}

Jika teman - teman perhatikan sebelum melakukan update quantity kita lakukan pengecekan terlebih dahulu yang dimana quantity cart harus lebih kecil dari quantity product, jika kondisi tersebut terpenuhi maka kita akan lakukan update quantity cart.

Selanjutnya jika pengecekan data cart bernilai false maka kita akan lakukan insert data baru kedalam tabel carts, tapi sebelum itu kita lakukan pengecekan quantity product harus lebih besar dari 0.

Pos/Index.php
else{
 	// do it when product quantity is more than 0
  if($product->quantity > 0)
      // create new cart data
      Cart::create([
          'user_id' => auth()->user()->id,
          'product_id' => $product->id,
          'quantity' => 1,
          'price' => $product->price,
      ]);
}

Terakhir kita akan diarahkan ke sebuah route yang bernama pos, disini kita memanfaat redirect dari livewire navigate true yang artinya kita akan berpindah halaman tanpa melakukan reload page.

Pos/Index.php
// render view
return $this->redirect('/pos', navigate: true);

Method incrementQty

Method ini kita gunakan untuk melakukan penambahan quantity product yang ada didalam data carts.

Pos/Index.php
// define method incrementQty
public function incrementQty($id)
{
    // get cart items by id
    $cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

    // get products by id
    $product = Product::findOrFail($cart->product_id);

    // do it when cart qty is less than product qty
    if($cart->quantity < $product->quantity){
        // update cart qty
        $cart->increment('quantity');
        // update totalQty
        $this->totalQty[$cart->id] = $cart->quantity;
        // update subTotalPrice
        $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
        // update totalPrice
        $this->totalPrice += $cart->price;
        // update change
        $this->change = $this->totalPrice;
    }
}

Pada method ini kita mendefinisikan sebuah paremeter id yang nantinya data tersebut kita gunakan untuk mendapatkan data cart.

Selanjutnya kita lakukan pencarian data cart yang sesuai dengan parameter id dan berdasarkan user yang sedang login saat ini.

Pos/Index.php
// get cart items by id
$cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

Kemudian kita lakukan pencarian data product sesuai dengan product_id yang ada didalam data carts.

Pos/Index.php
// get products by id
$product = Product::findOrFail($cart->product_id);

Berikutnya kita lakukan pengecekan yang dimanana quantity cart harus lebih kecil dari quantity product, jika nilainya true, maka kita akan lakukan update data carts kolom quantity.

Pos/Index.php
// do it when cart qty is less than product qty
if($cart->quantity < $product->quantity){
    // update cart qty
    $cart->increment('quantity');
    // update totalQty
    $this->totalQty[$cart->id] = $cart->quantity;
    // update subTotalPrice
    $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
    // update totalPrice
    $this->totalPrice += $cart->price;
    // update change
    $this->change = $this->totalPrice;
}

Selain itu kita juga lakukan assign data ke beberapa property yang telah kita definisikan kurang lebih seperti berikut ini :

  • totalQty

    Pos/Index.php
    // update totalQty
    $this->totalQty[$cart->id] = $cart->quantity;
    

    Pada property ini kita lakukan assign sesuai dengan key cart->id yang bernilai cart->quantity.

  • subTotalPrice

    Pos/Index.php
    // update subTotalPrice
    $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
    

    Pada property ini kita lakukan assign dengan key cart->id yang bernilai cart->quantity * cart->price.

  • totalPrice

    Pos/Index.php
    // update totalPrice
    $this->totalPrice += $cart->price;
    

    Pada property ini kita lakukan assign dengan nilai penambahan totalPrice dengan cart->price.

  • change

    Pos/Index.php
    // update change
    $this->change = $this->totalPrice;
    

    Pada property ini kita assign dengan nilai property totalPrice.

Method decrementQty

Method ini kita gunakan untuk melakukan pengurangan quantity product yang ada didalam data carts.

Pos/Index.php
// define method decrementQty
public function decrementQty($id)
{
    // get cart items by id
    $cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

    // do it when cart qty more than 1
    if($cart->quantity > 1){
        // update cart qty
        $cart->decrement('quantity');
        // update totalQty
        $this->totalQty[$cart->id] = $cart->quantity;
        // update subTotalPrice
        $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
        // update totalPrice
        $this->totalPrice -= $cart->price;
        // update change
        $this->change = $this->totalPrice;
    }
}

Pada method ini kita mendefinisikan sebuah paremeter id yang nantinya data tersebut kita gunakan untuk mendapatkan data cart.

Selanjutnya kita lakukan pencarian data cart yang sesuai dengan parameter id dan berdasarkan user yang sedang login saat ini.

Pos/Index.php
// get cart items by id
$cart = Cart::whereId($id)->whereUserId(auth()->user()->id)->first();

Kemudian kita lakukan pengecekan yang dimanana quantity cart harus lebih besar dari 1 , jika nilainya true maka kita akan lakukan update data carts kolom quantity.

Pos/Index.php
// do it when cart qty more than 1
if($cart->quantity > 1){
    // update cart qty
    $cart->decrement('quantity');
    // update totalQty
    $this->totalQty[$cart->id] = $cart->quantity;
    // update subTotalPrice
    $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
    // update totalPrice
    $this->totalPrice -= $cart->price;
    // update change
    $this->change = $this->totalPrice;
}

Selain itu kita juga lakukan assign data ke beberapa property yang telah kita definisikan kurang lebih seperti berikut ini :

  • totalQty

    Pos/Index.php
    // update totalQty
    $this->totalQty[$cart->id] = $cart->quantity;
    

    Pada property ini kita lakukan assign sesuai dengan key cart->id yang bernilai cart->quantity.

  • subTotalPrice

    Pos/Index.php
    // update subTotalPrice
    $this->subTotalPrice[$cart->id] = $cart->quantity * $cart->price;
    

    Pada property ini kita lakukan assign dengan key cart->id yang bernilai cart->quantity * cart->price.

  • totalPrice

    Pos/Index.php
    // update totalPrice
    $this->totalPrice -= $cart->price;
    

    Pada property ini kita lakukan assign dengan nilai pengurangan totalPrice dengan cart->price.

  • change

    Pos/Index.php
    // update change
    $this->change = $this->totalPrice;
    

    Pada property ini kita assign dengan nilai property totalPrice.

Method removeItem

Method ini kita gunakan untuk menghapus data carts sesuai dengan parameter id yang kita kirimkan.

Pos/Index.php
// define method removeItem
public function removeItem($id)
{
    // get cart data by id
    $cart = Cart::findOrFail($id);

    // delete cart data
    $cart->delete();

    // render view
    return $this->redirect('/pos', navigate: true);
}

Didalam method tersebut, pertama -tama kita mencari data cart yang sesuai berdasarkan paramater id yang dikirimkan.

Pos/Index.php
// get cart data by id
$cart = Cart::findOrFail($id);

Setelah data berhasil ditemukan, maka kita akan hapus data carttersebut.

Pos/Index.php
// delete cart data
$cart->delete();

Terakhir kita akan diarahkan ke sebuah route yang bernama pos, disini kita memanfaat redirect dari livewire navigate true yang artinya kita akan berpindah halaman tanpa melakukan reload page.

Pos/Index.php
// render view
return $this->redirect('/pos', navigate: true);

Method updateChange

Method ini kita gunakan untuk meghitung kembalian dari total transaksi.

Pos/Index.php
// define method updateChange
public function updateChange()
{
    // do it when property pay is more than 0
    if($this->pay > 0 )
        // calculate change
        $this->change = $this->pay - $this->totalPrice;
    else
        // change is totalPrice
        $this->change = $this->totalPrice;
}

Pada method ini, pertama kita lakukan pengecekan property pay harus lebih dari 0, jika nilainya true maka kita akan assign property change dengan nilai pengurangan dari property pay dengan property totalPrice.

Pos/Index.php
// do it when property pay is more than 0
if($this->pay > 0 )
    // calculate change
    $this->change = $this->pay - $this->totalPrice;

JIka nilai dari pengkondisiannya false, maka kita assign property change dengan nilai property totalPrice.

Pos/Index.php
else
    // change is totalPrice
    $this->change = $this->totalPrice;

Method updatePay

Method ini merupakan lifecycle hook yang dimiliki oleh livewire, jadi ketika ada perubahan pada property pay maka method ini akan dijalankan.

Pos/Index.php
// define lifecycle hook
public function updatedPay()
{
    // call method updateChange
    $this->updateChange();
}

Didalam method ini kita hanya melakukan pemanggilan method updateChange.

Method Save

Method ini kita gunakan untuk menyimpan data transaksi yang terjadi.

Pos/Index.php
// define method save
public function save()
{
    // start db transaction
    DB::transaction(function(){
        // loop
        for($i = 0; $i < 6; $i++)
            // generate random char
            $char = rand(0,1) ? rand(0, 9) : chr(rand(ord('a'), ord('z')));

        // generate invoice
        $invoice = 'TRX-'.str()->upper($char);

        // create new transaction
        $transaction = Transaction::create([
            'invoice' => $invoice,
            'user_id' => auth()->user()->id,
            'cash' => $this->pay,
            'grand_total' => $this->totalPrice,
            'change' => $this->change,
        ]);

        // loop user carts
        foreach($this->carts as $cart){
            // create new transaction details
            $transaction->details()->create([
                'product_id' => $cart->product_id,
                'quantity' => $cart->quantity,
                'price' => $cart->price
            ]);

            // update qty product
            Product::where('id', $cart->product_id)->decrement('quantity', $cart->quantity);
        }

        // delete cart
        $this->carts->each->delete();

        // render view
        return $this->redirect('/dashboard', navigate: true);
    });
}

Dikarenakan method ini akan melakukan beberapa transaksi yang meliputi lebih dari 1 tabel maka kita disini akan menggunakan DB:transaction.

Selanjutnya kita melakukan perulangan untuk mendapatkan sebuah data random string dan angka, yang hasil dari data tersebut kita simpan kedalam variabel invoice.

Pos/Index.php
// loop
for($i = 0; $i < 6; $i++)
    // generate random char
    $char = rand(0,1) ? rand(0, 9) : chr(rand(ord('a'), ord('z')));

// generate invoice
$invoice = 'TRX-'.str()->upper($char);

Berikutnya kita melakukan insert data baru kedalam tabel transactions.

Pos/Index.php
// create new transaction
$transaction = Transaction::create([
    'invoice' => $invoice,
    'user_id' => auth()->user()->id,
    'cash' => $this->pay,
    'grand_total' => $this->totalPrice,
    'change' => $this->change,
]);

Setelah data transaksi berhasil dibuat, kita lanjutkan dengan insert data baru kedalam tabel transaction_details, sebanyak data carts yang dimiliki oleh user yang sedang login.

Pos/Index.php
// loop user carts
foreach($this->carts as $cart){
    // create new transaction details
    $transaction->details()->create([
        'product_id' => $cart->product_id,
        'quantity' => $cart->quantity,
        'price' => $cart->price
    ]);

Selain kita melakukan insert data baru kedalam tabel transaction_details, kita juga melakukan pengurangan data quantity product sesuai dengan data product yang ada didalam data carts.

Pos/Index.php
// update qty product
Product::where('id', $cart->product_id)->decrement('quantity', $cart->quantity);

Berikutnya kita hapus seluruh data carts user yang sedang login.

Pos/Index.php
// delete cart
$this->carts->each->delete();

Terakhir jika seluruh transaksi diatas berhasil dilakukan kita akan diarahkan ke halaman dashboard , dengan memanfaatkan redirect dari livewire navigate true yang artinya kita akan berpindah halaman tanpa melakukan reload page.

Pos/Index.php
// render view
return $this->redirect('/dashboard', navigate: true);

Method Render

Method ini kita gunakan untuk menampilkan data product yang kita miliki didalam database.

Products/Index.php
public function render()
{
    // list products data
    $products = Product::query()
        ->with('category')
        ->when($this->search, function($query){
            $query->where('name', 'like', '%'. $this->search . '%');
        })->paginate(9);

    // render view
    return view('livewire.pages.pos.index', compact('products'));
}

Pada kode diatas, kita membuat sebuah variabel baru dengan nama products yang dimana, variabel tersebut menampung seluruh data product yang kita miliki, disini kita juga melakukan filtering data sesuai dengan value dari property search, kemudian kita buat pembatasan data yang ditampilkan hanya sebanyak 9 data menggunakan method paginate.

Products/Index.php
// list products data
$products = Product::query()
      ->with('category')
      ->when($this->search, function($query){
          $query->where('name', 'like', '%'. $this->search . '%');
      })->paginate(9);

Kemudian, data products tersebut kita lempar kedalam view dengan cara menggunakan sebuah method compact, agar data tersebut bisa kita gunakan didalam view.

Products/Index.php
// render view
return view('livewire.pages.pos.index', compact('products'));

Route Component Pos Index

Setelah berhasil membuat component pos index, sekarang kita akan lanjutkan untuk membuat route-nya, Silahkan teman - teman buka file routes/web.php, kemudian tambahkan kode berikut ini :

web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Livewire\Pages\Categories\Index as CategoryIndex;
use App\Livewire\Pages\Categories\Create as CategoryCreate;
use App\Livewire\Pages\Categories\Edit as CategoryEdit;
use App\Livewire\Pages\Products\Index as ProductIndex;
use App\Livewire\Pages\Products\Create as ProductCreate;
use App\Livewire\Pages\Products\Edit as ProductEdit;
use App\Livewire\Pages\Pos\Index as PosIndex;


Route::view('/', 'welcome');

Route::group(['middleware' => ['auth']], function(){
    // categories route
    Route::group(['prefix' => 'categories', 'as' => 'categories.'], function(){
        Route::get('/', CategoryIndex::class)->name('index');
        Route::get('/create', CategoryCreate::class)->name('create');
        Route::get('/edit/{category}', CategoryEdit::class)->name('edit');
    });
    // products route
    Route::group(['prefix' => 'products', 'as' => 'products.'], function(){
        Route::get('/', ProductIndex::class)->name('index');
        Route::get('/create', ProductCreate::class)->name('create');
        Route::get('/edit/{product}', ProductEdit::class)->name('edit');
    });
   // pos route
    Route::get('/pos', PosIndex::class)->name('pos');
});

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

Route::view('profile', 'profile')
    ->middleware(['auth'])
    ->name('profile');

require __DIR__.'/auth.php';

Dari perubahan kode diatas, kita menambahkan sebuah route baru dengan nama pos, untuk memastikan route yang kita buat berfungsi, teman - teman bisa jalankan perintah berikut ini dalam terminal.

Terminal
php artisan r:l --name=pos

Setelah perintah artisan diatas berhasil dijalankan maka kita akan mendapatkan output, kurang lebih seperti berikut ini.

Terminal
GET|HEAD       pos ............................................................... pos › App\Livewire\Pages\Pos\Index

Navigasi Component Pos Index

Silahkan teman - teman buka file yang bernama navigation.blade.php yang terletak di folder resources/views/livewire/layout, kemudian ubah kodenya menjadi berikut ini :

navigation.blade.php
<?php

use App\Livewire\Actions\Logout;
use Livewire\Volt\Component;

new class extends Component
{
    /**
     * Log the current user out of the application.
     */
    public function logout(Logout $logout): void
    {
        $logout();

        $this->redirect('/', navigate: true);
    }
}; ?>

<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
    <!-- Primary Navigation Menu -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}" wire:navigate>
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
                    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
                        {{ __('Dashboard') }}
                    </x-nav-link>
                    <x-nav-link :href="route('categories.index')" :active="request()->routeIs('categories*')" wire:navigate>
                        {{ __('Categories') }}
                    </x-nav-link>
                    <x-nav-link :href="route('products.index')" :active="request()->routeIs('products*')" wire:navigate>
                        {{ __('Products') }}
                    </x-nav-link>
                    <x-nav-link :href="route('pos')" :active="request()->routeIs('pos*')" wire:navigate>
                        {{ __('Point of Sales') }}
                    </x-nav-link>
                </div>
            </div>

            <!-- Settings Dropdown -->
            <div class="hidden sm:flex sm:items-center sm:ms-6">
                <x-dropdown align="right" width="48">
                    <x-slot name="trigger">
                        <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
                            <div x-data="{{ json_encode(['name' => auth()->user()->name]) }}" x-text="name" x-on:profile-updated.window="name = $event.detail.name"></div>

                            <div class="ms-1">
                                <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                    <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
                                </svg>
                            </div>
                        </button>
                    </x-slot>

                    <x-slot name="content">
                        <x-dropdown-link :href="route('categories.index')" wire:navigate>
                            {{ __('Categories') }}
                        </x-dropdown-link>
                        <x-dropdown-link :href="route('products.index')" wire:navigate>
                            {{ __('Products') }}
                        </x-dropdown-link>
                        <x-dropdown-link :href="route('pos')" wire:navigate>
                            {{ __('Point of Sales') }}
                        </x-dropdown-link>
                        <x-dropdown-link :href="route('profile')" wire:navigate>
                            {{ __('Profile') }}
                        </x-dropdown-link>

                        <!-- Authentication -->
                        <button wire:click="logout" class="w-full text-start">
                            <x-dropdown-link>
                                {{ __('Log Out') }}
                            </x-dropdown-link>
                        </button>
                    </x-slot>
                </x-dropdown>
            </div>

            <!-- Hamburger -->
            <div class="-me-2 flex items-center sm:hidden">
                <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
                    <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                        <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                        <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </button>
            </div>
        </div>
    </div>

    <!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
                {{ __('Dashboard') }}
            </x-responsive-nav-link>
            <x-responsive-nav-link :href="route('categories.index')" :active="request()->routeIs('categories*')" wire:navigate>
                {{ __('Categories') }}
            </x-responsive-nav-link>
            <x-responsive-nav-link :href="route('products.index')" :active="request()->routeIs('products*')" wire:navigate>
                {{ __('Products') }}
            </x-responsive-nav-link>
            <x-responsive-nav-link :href="route('pos')" :active="request()->routeIs('pos')" wire:navigate>
                {{ __('Point of Sales') }}
            </x-responsive-nav-link>
        </div>

        <!-- Responsive Settings Options -->
        <div class="pt-4 pb-1 border-t border-gray-200">
            <div class="px-4">
                <div class="font-medium text-base text-gray-800" x-data="{{ json_encode(['name' => auth()->user()->name]) }}" x-text="name" x-on:profile-updated.window="name = $event.detail.name"></div>
                <div class="font-medium text-sm text-gray-500">{{ auth()->user()->email }}</div>
            </div>
            <x-responsive-nav-link :href="route('categories.index')" wire:navigate>
                {{ __('Categories') }}
            </x-responsive-nav-link>
            <x-responsive-nav-link :href="route('products.index')" wire:navigate>
                {{ __('Products') }}
            </x-responsive-nav-link>
            <x-responsive-nav-link :href="route('pos')" wire:navigate>
                {{ __('Point of Sales') }}
            </x-responsive-nav-link>
            <div class="mt-3 space-y-1">
                <x-responsive-nav-link :href="route('profile')" wire:navigate>
                    {{ __('Profile') }}
                </x-responsive-nav-link>

                <!-- Authentication -->
                <button wire:click="logout" class="w-full text-start">
                    <x-responsive-nav-link>
                        {{ __('Log Out') }}
                    </x-responsive-nav-link>
                </button>
            </div>
        </div>
    </div>
</nav>

Pada kode diatas, kita menambahkan beberapa navigasi yang mengarah ke route pos , dengan menggunakan, beberapa component yang telah disediakan oleh laravel yaitu :

  1. <x-nav-link/>.
  2. <x-dropdown-link/>.
  3. <x-responsive-nav-link/>

View Component Pos Index

Silahkan teman - teman buka file yang bernama index.blade.php yang terletak di dalam folder resources/views/livewire/pages/pos, kemudian tambahkan kode berikut ini :

index.blade.php
<div>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Products') }}
        </h2>
    </x-slot>

    <div class="py-12 px-4">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="mb-4 flex items-center justify-between gap-4">
                <x-button type="create" :href="route('products.create')">
                    <x-slot name="title">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
                            fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
                            stroke-linejoin="round"
                            class="icon icon-tabler icons-tabler-outline icon-tabler-circle-plus">
                            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                            <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
                            <path d="M9 12h6" />
                            <path d="M12 9v6" />
                        </svg>
                        <span class="hidden md:block">Create New Product</span>
                    </x-slot>
                </x-button>
                <div class="w-full md:w-1/2">
                    <x-search placeholder="Search products by name, price, or category.."/>
                </div>
            </div>
            <x-table :heads="$table_heads" title="List Data Products">
                @forelse ($products as $key => $product)
                    <tr>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            {{ $key + $products->firstItem() }}</td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            {{ $product->name }}
                        </td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            <img src="{{ $product->image }}" alt="{{ $product->name }}"
                                class="object-cover w-10 h-10 rounded-lg" />
                        </td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            {{ $product->category->name }}
                        </td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            <sup>Rp</sup> {{ number_format($product->price, 0) }}
                        </td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            {{ $product->quantity }}
                        </td>
                        <td class="whitespace-nowrap px-6 py-2 text-gray-700 rounded-b-lg text-sm">
                            <div class="flex items-center gap-2">
                                <x-button type="edit" title="Edit" :href="route('products.edit', $product->id)"/>
                                <x-button type="delete" title="Delete" id="delete({{ $product->id }})"/>
                            </div>
                        </td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="7"
                            class="whitespace-nowrap px-6 py-2 text-rose-700 rounded-b-lg text-sm text-center">
                            Sorry, we couldn't find anything...
                        </td>
                    </tr>
                @endforelse
            </x-table>
            <div class="mt-4">
                {{ $products->links() }}
            </div>
        </div>
    </div>
</div>

Pada kode diatas, kita melakukan pemanggilan beberapa components yang telah kita buat sebelumnya diantaranya :

  • <x-button/> - props yang digunakan type, href dan title.
  • <x-search/> - props yang digunakan placeholder.
  • <x-table/> - props yang digunakan heads dan title.
  • <x-modal/> - props yang digunakan type.

Untuk pencarian data, kita menggunakan component search yang kita buat sebelumnya, dengan kode kurang lebih seperti berikut ini :

search.blade.php
@props(['placeholder' => ''])

<div class="relative flex w-full flex-col gap-1 text-gray-400">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true" class="absolute left-2.5 top-1/2 size-5 -translate-y-1/2 text-gray-700/50">
        <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
    </svg>
    <input
        type="test"
        class="w-full rounded-lg border border-gray-200 text-gray-700 py-2 px-4 pl-10 pr-2 text-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-750"
        wire:model.live="search"
        placeholder="{{ $placeholder }}"
        aria-label="search"
    />
</div>

Pada kode daitas, kita membuat sebuah element input dengan attribute bawaan livewire wire:model.live dengan value search. Disini sehingga jika element input kita masukan kata kunci, dia akan langsung mencari data sesuai dengan property search yang telah kita definisikan di component pos index.

Untuk paginate halaman, kita buat kurang lebih seperti berikut ini :

index.blade.php
<div class="mt-4">
  {{ $products->links(data: ['scrollTo' => false]) }}
</div>

Dan untuk menampilkan perulangan data kita menggunakan kode, kurang lebih seperti berikut ini :

index.blade.php
@forelse ($products as $key => $product)
  	// ....
@empty
  	// ....
@endforelse

Pada kode diatas, variabel products kita dapatkan dari passing data yang ada di component pos index didalam method render().

pos-view

Pada kode ini, kita mendefiniskan sebuah wire:click yang tertuju pada method addToCart dengan parameter product->id yang ada di component pos index.

index.blade.php
<button wire:click="addToCart({{ $product->id }})"
    class="bg-white border rounded-lg relative">
    <img src="{{ $product->image }}" alt="{{ $product->title }}"
        class="rounded-lg rounded-b-none" />
    <div
        class="top-0 absolute left-0 font-mono bg-rose-300/40 w-10 rounded-r-2xl text-rose-500 rounded-tl-lg border-rose-500 border">
        {{ $product->qty }}
    </div>
    <div class="p-4">
        <div class="font-semibold">
            {{ $product->name }}
        </div>
        <p class="text-sm text-gray-500 mb-2">{{ $product->category->name }}</p>
        <div class="font-mono">
            <sup>Rp</sup> {{ number_format($product->price, 0) }}
        </div>
    </div>
</button>

Selanjutnya pada kode ini, kita mendefinisikan sebuah wire:click yang tertuju pada method removeItem dengan parameter cart->id yang ada di component pos index.

index.blade.php
<button wire:click="removeItem({{ $cart->id }})" class="rounded-lg p-2 border border-rose-500 hover:bg-rose-200">
    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
        viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
        stroke-linecap="round" stroke-linejoin="round"
        class="icon icon-tabler icons-tabler-outline icon-tabler-trash text-rose-500">
        <path stroke="none" d="M0 0h24v24H0z" fill="none" />
        <path d="M4 7l16 0" />
        <path d="M10 11l0 6" />
        <path d="M14 11l0 6" />
        <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
        <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
    </svg>
</button>

Kemudian pada kode ini, kita mendefinisikan beberapa wire:click dan wire:model, diantaranya sebagai berikut ini :

  • wire:click dengan method decrementQty beserta parameter cart->id.
  • wire:model dengan property totalQty dengan key cart->id.
  • wire:click dengan method incrementQty beserta parameter cart->id.
index.blade.php
<div class="flex items-center">
    <button wire:click="decrementQty({{ $cart->id }})"
        class=" bg-white hover:bg-gray-100 flex h-4 items-center justify-center rounded-l-md border p-2 text-slate-700 hover:opacity-75 focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 active:opacity-100 active:outline-offset-0"
        aria-label="subtract">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
            aria-hidden="true" stroke="currentColor" fill="none"
            stroke-width="1.5" class="size-4">
            <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
        </svg>
    </button>
    <input wire:model="totalQty.{{ $cart->id }}" type="text"
        class="h-4 w-10 rounded-none text-xs font-mono text-center text-black border-t border-b border-r-0 border-l-0 border-gray-200 focus:ring-0 focus:border-gray-200"
        readonly />
    <button wire:click="incrementQty({{ $cart->id }})"
        class="bg-white hover:bg-gray-100 flex h-4 items-center justify-center rounded-r-md border p-2 text-slate-700 hover:opacity-75 focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 active:opacity-100 active:outline-offset-0"
        aria-label="add">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
            aria-hidden="true" stroke="currentColor" fill="none"
            stroke-width="1.5" class="size-4">
            <path stroke-linecap="round" stroke-linejoin="round"
                d="M12 4.5v15m7.5-7.5h-15" />
        </svg>
    </button>
</div>

Terakhir pada component modal, kita mendefinisikan form action kita menggunakan attribute yang disediakan oleh livewire yaitu wire:submit.prevent kemudian kita set valuenya dengan method save yang telah kita buat sebelumnya.

index.blade.php
<x-modal type="transaction">
    <div class="p-4">
        <form wire:submit.prevent="save">
            <div class="flex items-center justify-between gap-2 border-b px-4 py-3 border-dashed border-gray-700">
                <div class="font-semibold text-sm">
                    Grand Total
                </div>
                <div class="font-mono text-sm">
                     <sup>Rp</sup>{{ number_format($totalPrice, 0) }}
                </div>
            </div>
            <div class="flex items-center justify-between gap-2 border-b px-4 py-3 border-dashed border-gray-700">
                <div class="font-semibold text-sm">
                    Pay
                </div>
                <div class="flex items-center justify-end">
                    <input type="number" min="0" wire:model.live="pay" class="w-2/3 h-1/2 border-0 border-b border-slate-700 bg-slate-800 text-sm text-end font-mono focus:ring-0 focus:border-b-gray-700"/>
               </div>
            </div>
            <div class="flex items-center justify-between gap-2 px-4 py-3 border-b  border-dashed border-gray-700">
                <div class="font-semibold text-sm">
                    Change
                </div>
                <div class="font-mono text-sm">
                     <sup>Rp</sup>{{ number_format($change, 0) }}
                </div>
            </div>
            <div class="mt-3">
                <button
                    type="submit"
                    class="{{ $pay >= $totalPrice ? 'bg-gray-900 text-gray-50' : 'cursor-not-allowed bg-slate-900 text-gray-500' }} text-sm text-center w-full px-3 py-1.5 border rounded-md focus:outline-none focus:ring-0 border-gray-700"
                    {{ $pay >= $totalPrice ? '' : 'disabled' }}>
                    {{ $pay >= $totalPrice ? 'Save Transaction' : 'Please complete the payment first' }}
                </button>
            </div>
        </form>
    </div>
</x-modal>

Screenshoot Hasil

pos-view

pos-view

pos-modal

pos-modal

pos-transactio-success

Penutup

Pada artikel kali ini kita telah berhasil menyelesaikan pembuatan module pos, jika teman - teman setelah melakukan transaksi medapati error seperti diatas jangan khawatir hal tersebut terjadi dikarenakan kita belum melakukan perubahan pada component dashoard kita, pada artikel selanjutnya kita akan membuat module dashboard.

Series Artikel

Berikut ini daftar series artikel dari Tutorial Point of Sales Laravel, Livewire, Alpine.js & Tailwind Css

1
Tutorial Laravel Livewire - #1 - Project Overview

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan bahasa teknologi apa saja yang kita gunakan dan scope dari project yang akan kita bangun.

2
Tutorial Laravel Livewire - #2 - Installasi Project Laravel

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan mulai melakukan installasi project laravel kita dari awal menggunakan composer.

3
Tutorial Laravel Livewire - #3 - Installasi Laravel Breeze & Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan melakukan installasi laravel breeze dan livewire sebagai starter-kit kita.

4
Tutorial Laravel Livewire - #4 - Membuat Schema Database Dengan Laravel

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah schema database yang nantinya akan kita gunakan pada study case kali ini dan kita akan memanfaatkan model dan migration dari laravel.

5
Tutorial Laravel Livewire - #5 - Membuat Relasi Antar Table Dengan Laravel

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat relasi antar tabel menggunakan eloquent relationship yang telah disedikan oleh laravel.

6
Tutorial Laravel Livewire - #6 - Memanfaatkan Salah Satu Magic Laravel Yaitu Eloquent Accessor

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan mengenal salah satu magic laravel yang sangat powerfull yaitu laravel accessor.

7
Tutorial Laravel Livewire - #7 - Membuat Reusable Component Dengan Laravel

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita membuat sebuah reusable component menggunakan laravel.

8
Tutorial Laravel Livewire - #8 - Membuat Module Category Dengan Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah module category dengan livewire.

9
Tutorial Laravel Livewire - #9 - Membuat Module Product Dengan Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah module product dengan livewire.

10
Tutorial Laravel Livewire - #10 - Membuat Module POS Dengan Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah module pos dengan livewire.

11
Tutorial Laravel Livewire - #11 - Membuat Module Dashboard Dengan Livewire

Artikel ini merupakan series dari Tutorial Laravel Livewire Study Case Point Of Sales, disini kita akan membuat sebuah module dashboard dengan livewire.

JurnalKoding

Mulai asah skill dengan berbagai macam teknologi - teknologi terbaru seperti Laravel, React, Vue, Inertia, Tailwind CSS, dan masih banyak lagi.

© 2024 JurnalKoding, Inc. All rights reserved.