laravel-jquery-pos-cart-system

Laravel jQuery POS Tutorial – Part 5/8: POS Cart System with jQuery

Introduction

In this Laravel jQuery POS cart system tutorial, you’ll learn how to build a fully interactive POS cart system using jQuery and Ajax in your Laravel application. This cart system allows cashiers to add products, adjust quantities, and remove items—all without refreshing the page—providing a fast and seamless POS experience.

You’ll walk through each step, including setting up jQuery Ajax functions, creating a cart model (optional), building a controller to manage cart actions, designing a dynamic Blade view for real-time updates, and defining routes to connect everything together.

By the end of this tutorial, your Laravel POS with jQuery project will be equipped with a working cart ready for real-world transactions.

Setup jQuery Ajax Function for Laravel jQuery POS Cart System

In this step, you’ll integrate jQuery Ajax into your Laravel POS with jQuery project to create seamless communication between the frontend and backend. With Ajax, actions like adding or removing items from the cart happen instantly—without reloading the page—giving your POS system a fast and modern user experience.

Replace the contents of resources/js/cashier.js with the code below. If the file doesn’t exist yet, please create it.

// Laravel jQuery POS cart system @ https://laravelcenter.com
import './bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.min.css';
import '../css/style.css';
import '../css/app.css';
import * as bootstrap from 'bootstrap';
import jQuery from 'jquery';
window.bootstrap = bootstrap;
window.$ = jQuery;
import printJS from 'print-js';

$(function () {
    $("body").show();
    getProduct();
    selectTable();
});

window.selectAll = (event) => {
    $('.item').prop('checked', event.target.checked);
    $('#change_table').prop('disabled', !event.target.checked);
};

window.checkItem = () => {
    const total = $('.item').length;
    const checked = $('.item:checked').length;

    if (checked === 0) {
        $('#selectAll').prop('checked', false).prop('indeterminate', false);
        $('#change_table').prop('disabled', true);
    } else if (checked === total) {
        $('#selectAll').prop('checked', true).prop('indeterminate', false);
        $('#change_table').prop('disabled', false);
    } else {
        $('#selectAll').prop('checked', false).prop('indeterminate', true);
        $('#change_table').prop('disabled', false);
    }
};

const formModalId = document.getElementById("formModal");
const formModal = new bootstrap.Modal(formModalId);
if (formModalId) {
    formModalId.addEventListener("shown.bs.modal", (event) => {
        $("#autofocus").trigger("focus").trigger("select");
    });
    formModalId.addEventListener("hide.bs.modal", (event) => {
        document.activeElement?.blur();
    });
}

window.ajaxPopup = function (url, bigModal = true) {
    $(".loading").show();
    $.ajax({
        type: "GET",
        url: url,
        contentType: false,
        success: function (data) {
            if (bigModal)
                $("#formModal .modal-dialog").removeClass('modal-lg').addClass('modal-lg');
            else
                $("#formModal .modal-dialog").removeClass('modal-lg');
            $("#formModal .modal-content").html(data);
            formModal.show();
        },
        error: function (xhr, status, error) {
            showError(xhr.responseJSON.message);
        },
        complete: function () {
            $(".loading").hide();
        }
    });
};

const deleteModal = document.getElementById("confirmDelete");
if (deleteModal) {
    deleteModal.addEventListener("show.bs.modal", (event) => {
        var data = $(event.relatedTarget).data();
        $("input#delete_id").val(data.recordId);
        document.querySelector('form#deleteForm').action = data.recordUrl;
    });
    deleteModal.addEventListener("hide.bs.modal", (event) => {
        document.activeElement?.blur();
    });
}

const errorModalId = document.getElementById("errorModal");
const errorModal = new bootstrap.Modal(errorModalId);
if (errorModalId) {
    errorModalId.addEventListener("hide.bs.modal", (event) => {
        document.activeElement?.blur();
    });
}

window.addToOrder = function (product_id) {
    ajaxSubmit("cashier/add-to-order", { id: product_id, }, "orderList");
};

$(document).on("submit", "form#deleteForm", function (event) {
    event.preventDefault();
    $(".loading").show();
    var form = $(this);
    var data = new FormData(form[0]);
    var url = form.attr("action");
    $.ajaxSetup({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
        },
    });
    $.ajax({
        type: "POST",
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        success: function (data) {
            $("#orderList").html(data);
            $(".loading").hide();
        },
        error: function (xhr, textStatus, errorThrown) {
            showError(xhr.responseJSON.message);
        },
    });
    return false;
});

window.ajaxLoad = function (url, content) {
    content = typeof content !== "undefined" ? content : "content";
    $(".loading").show();
    $.ajax({
        type: "GET",
        url: url,
        contentType: false,
        success: function (data) {
            $("#" + content).html(data);
        },
        error: function (xhr, status, error) {
            showError(xhr.responseJSON.message);
        },
        complete: function () {
            $(".loading").hide();
        }
    });
};

window.getProduct = (event = null) => {
    if (event) {
        $("span.menu-item").removeClass('active');
        $(event.target).addClass('active');
    }
    var category = $('.menu-item.active').data('category');
    var search = $('#search_product').val();
    ajaxLoad("cashier/product/" + category + "?search=" + search, "productList");
};

window.selectTable = function (new_table_id = 0, old_table_id = 0) {
    formModal.hide();
    let ids = $('.item:checked').map(function () {
        return $(this).val();
    }).get().join();
    ajaxSubmit("cashier/select-table", {
        old_table_id: old_table_id,
        new_table_id: new_table_id,
        ids: ids
    }, "orderList");
};

window.ajaxSubmit = function (url, data, content = "orderList") {
    $(".loading").show();
    $.ajaxSetup({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
        },
    });
    $.ajax({
        url: url,
        type: "POST",
        data: data,
        success: function (data) {
            if (data == "NOTABLE")
                showError("Please select any table first!");
            else
                $("#" + content).html(data);
        },
        error: function (xhr, status, error) {
            showError(xhr.responseJSON.message);
        },
        complete: function () {
            $(".loading").hide();
        }
    });
};

window.ajaxPrint = function (url, data, content = "printContent") {
    $(".loading").show();
    $.ajaxSetup({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
        },
    });
    $.ajax({
        url: url,
        type: "POST",
        data: data,
        success: function (data) {
            $("#" + content).html(data);
        },
        error: function (xhr, status, error) {
            showError(xhr.responseJSON.message);
        },
        complete: function () {
            printJS({
                printable: content,
                type: "html",
                scanStyles: false,
                style: "#" + content + "{ display: block !important; }"
            });
            $(".loading").hide();
        }
    });
};

$(document).on("submit", "form#paymentForm", function (event) {
    event.preventDefault();
    $(".loading").show();
    var form = $(this);
    var data = new FormData(form[0]);
    var url = form.attr("action");
    $.ajaxSetup({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
        },
    });
    $.ajax({
        type: "POST",
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        success: function (data) {
            if (!data.success) {
                $("#receive_amount_error").text(data.errors['receive_amount'][0]);
                $("#autofocus").trigger("focus").trigger("select");
            } else {
                formModal.hide();
                $("#printContent").html(data.content);
                printJS({
                    printable: "printContent",
                    type: "html",
                    scanStyles: false,
                    style: "#printContent{ display: block !important; }"
                });
                selectTable();
            }
        },
        error: function (xhr, textStatus, errorThrown) {
            showError(xhr.responseJSON.message);
        },
        complete: function () {
            $(".loading").hide();
        }
    });
    return false;
});

window.showError = (msg) => {
    $('.modal-body p', errorModalId).text(msg);
    errorModal.show();
}

const successModalId = document.getElementById("successModal");
const successModal = new bootstrap.Modal(successModalId);
if (successModalId) {
    successModalId.addEventListener("hide.bs.modal", (event) => {
        document.activeElement?.blur();
    });
}

$(document).on("submit", "form#submitForm", function (event) {
    event.preventDefault();
    $(".loading").show();
    var form = $(this);
    var data = new FormData(form[0]);
    var url = form.attr("action");
    $.ajaxSetup({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
        },
    });
    $.ajax({
        type: "POST",
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        success: function (data) {
            $(".is-invalid").removeClass("is-invalid");
            $("span.invalid-feedback").remove();
            if (!data.success) {
                for (var control in data.errors) {
                    $("[name='" + control + "']").addClass("is-invalid");
                    $("<span class='invalid-feedback'>" + data.errors[control] + "</span>").insertAfter($("[name='" + control + "']"));
                    $("#autofocus").trigger("focus");
                }
            } else {
                formModal.hide();
                $('.modal-body p', successModalId).text("Data has been saved successfully");
                successModal.show();
                if (data.redirect_url) {
                    ajaxLoad(data.redirect_url);
                }
            }
            $(".loading").hide();
        },
        error: function (xhr, textStatus, errorThrown) {
            showError(xhr.responseJSON.message);
        },
    });
    return false;
}); 

Setup Model in Laravel jQuery POS Cart System

While many Laravel POS Cart System use session storage for cart data, creating a dedicated model offers more flexibility. This model can store product selections, quantities, and pricing in the database, allowing your POS to support reporting, syncing, and multi-user access.

Replace the contents of app/Models/OrderDetailTemp.php with the code below. If the file doesn’t exist yet, please create it.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class OrderDetailTemp extends Model
{
    public function table()
    {
        return $this->belongsTo(Table::class);
    }
}

Replace the contents of app/Models/OrderDetail.php with the code below. If the file doesn’t exist yet, please create it.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class OrderDetail extends Model
{
    use  SoftDeletes;
}

Replace the contents of app/Models/Order.php with the code below. If the file doesn’t exist yet, please create it.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model
{
    use  SoftDeletes;

    public function order_details()
    {
        return $this->hasMany(OrderDetail::class);
    }

    public function table()
    {
        return $this->belongsTo(Table::class);
    }

    public function createdBy()
    {
        return $this->belongsTo(User::class, 'updated_by_id', 'id');
    }
}

Replace the contents of app/Models/Table.php with the code below. If the file doesn’t exist yet, please create it.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Table extends Model
{
    use SoftDeletes;

    public function order_detail_temps()
    {
        return $this->hasMany(OrderDetailTemp::class);
    }

    public function createdBy()
    {
        return $this->belongsTo(User::class, 'updated_by_id', 'id');
    }
}

Setup Controller for Laravel jQuery POS Cart System Logic

The controller is where your POS Cart System app processes Ajax requests. You’ll create methods to add items to the cart, update quantities, and remove products—all while managing the session and returning updated views. It’s the core logic behind your dynamic POS cart system.

Replace the contents of app/Http/Controllers/CashierController.php with the code below. If the file doesn’t exist yet, please create it.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\OrderDetailTemp;
use App\Models\Product;
use App\Models\ProductCategory;
use App\Models\Table;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;

class CashierController extends Controller
{
    public function index(Request $request)
    {
        $product_categories = ProductCategory::select('id', 'name')->orderBy('ordering')->orderBy('name')->get();
        $products = Product::orderBy('name')->get();
        return view('layout.cashier', compact('products', 'product_categories'));
    }

    public function product(Request $request)
    {
        $list = Product::when($request->category > 0, function ($query) use ($request) {
            $query->where('product_category_id', $request->category);
        })->when($request->search > 0, function ($query) use ($request) {
            $query->where('name', 'like', '%' . $request->search . '%');
        })->orderBy('name')->get();

        return view('cashier.product_list', compact('list'));
    }

    public function table($id = 0)
    {
        $old_table_id = $id;
        $list = Table::select('id', 'name', 'status')->where('id', '!=', $id)->orderBy('name')->get();
        return view('cashier.table_list', compact('list', 'old_table_id'));
    }

    public function selectTable(Request $request)
    {
        try {
            DB::beginTransaction();
            if ($request->old_table_id > 0 && $request->ids) {
                $old_table = Table::with('order_detail_temps')->find($request->old_table_id);
                Table::where('id', $request->new_table_id)->update(['status' => 1, 'discount' => $old_table->discount]);
                if ($old_table->order_detail_temps->count() == 0) {
                    $old_table->status = 2;
                    $old_table->discount = 0;
                    $old_table->total_discount = 0;
                    $old_table->grand_total = 0;
                    $old_table->total = 0;
                    $old_table->net_amount = 0;
                } else {
                    $old_table->order_detail_temps()->whereIn('id', explode(',', $request->ids))->update(['table_id' => $request->new_table_id]);
                    $grand_total = $old_table->order_detail_temps->whereNotIn('id', explode(',', $request->ids))->sum(function ($detail) {
                        return $detail->qty * $detail->unit_price;
                    });
                    $total = $old_table->order_detail_temps->whereNotIn('id', explode(',', $request->ids))->sum(function ($detail) {
                        return $detail->qty * $detail->unit_price * (1 - $detail->discount / 100);
                    });
                    $total_discount = $grand_total - $total + ($total * $old_table->discount / 100);
                    $old_table->grand_total = $grand_total;
                    $old_table->total = $total;
                    $old_table->total_discount = $total_discount;
                    $old_table->net_amount = $grand_total - $total_discount;
                }
                $old_table->save();
            }
            if ($request->new_table_id > 0)
                session()->put('table_id', $request->new_table_id);
            $data = Table::with('order_detail_temps')->find(session('table_id'));
            if ($data && $data->order_detail_temps->count() > 0) {
                $grand_total = $data->order_detail_temps->sum(function ($detail) {
                    return $detail->qty * $detail->unit_price;
                });

                $total = $data->order_detail_temps->sum(function ($detail) {
                    return $detail->qty * $detail->unit_price * (1 - $detail->discount / 100);
                });
                $total_discount = $grand_total - $total + ($total * $data->discount / 100);
                $data->grand_total = $grand_total;
                $data->total = $total;
                $data->total_discount = $total_discount;
                $data->net_amount = $grand_total - $total_discount;
                $data->save();
            }
            DB::commit();
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
            DB::rollBack();
        }
        return view('cashier.order_list', compact('data'));
    }

    public function addToOrder(Request $request)
    {
        if (!session('table_id')) {
            return 'NOTABLE';
        }
        $product = Product::with('product_category')->find($request->id);
        try {
            DB::beginTransaction();
            $order_detail = OrderDetailTemp::where('table_id', session('table_id'))->where('product_id', $product->id)->first();
            if (!$order_detail) {
                $order_detail = new OrderDetailTemp();
                $order_detail->table_id = session('table_id');
                $order_detail->product_id = $product->id;
                $order_detail->product_category_id = $product->product_category_id;
                $order_detail->qty = 1;
                $order_detail->description = $product->name;
                $order_detail->unit_price = $product->unit_price;
                $order_detail->created_by_id = $request->user()->id;
            } else {
                $order_detail->qty += 1;
            }
            $order_detail->updated_by_id = $request->user()->id;
            $order_detail->table->status = 1;
            $order_detail->push();
            DB::commit();
        } catch (Exception $ex) {
            DB::rollBack();
            abort(500, $ex->getMessage());
        }
        return $this->selectTable($request);
    }

    public function deleteOrderProduct(Request $request)
    {
        try {
            OrderDetailTemp::destroy($request->delete_id);
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
        }
        return $this->selectTable($request);
    }

    public function updateOrderQty(Request $request)
    {
        try {
            $data = OrderDetailTemp::find($request->id);
            $data->qty = $request->qty;
            $data->save();
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
        }
        return $this->selectTable($request);
    }

    public function updateDetailDiscount(Request $request)
    {
        try {
            $data = OrderDetailTemp::find($request->id);
            $data->discount = $request->discount;
            $data->save();
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
        }
        return $this->selectTable($request);
    }

    public function updateDiscount(Request $request)
    {
        try {
            $table = Table::find(session('table_id'));
            $table->discount = $request->discount;
            $table->save();
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
        }
        return $this->selectTable($request);
    }

    public function printInvoice(Request $request)
    {
        try {
            $data = Table::find($request->table_id);
            $data->invoice_no = date('YmdHi') . $request->table_id;
            $data->status = 3;
            $data->save();
        } catch (Exception $ex) {
            abort(500, $ex->getMessage());
        }
        return view('cashier.print_invoice', compact('data'));
    }

    public function makePayment(Request $request)
    {
        if ($request->isMethod('GET')) {
            $data = Table::find(session('table_id'));
            return view('cashier.make_payment', compact('data'));
        }
        $table = Table::find($request->table_id);

        // validation
        $rules = [
            'receive_amount' => 'required|numeric|min:' . $table->net_amount
        ];
        $validator = Validator::make($request->all(), $rules, [
            'receive_amount.required' => 'is required',
            'receive_amount.numeric' => 'must be number',
            'receive_amount.min' => 'must be at least ' . $table->net_amount,
        ]);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        try {
            DB::beginTransaction();
            $order = new Order();
            $order->table_id = $table->id;
            $order->invoice_no = $table->invoice_no ? $table->invoice_no : (date('YmdHi') . $request->table_id);
            $order->discount = $table->discount;
            $order->total_discount = $table->total_discount;
            $order->grand_total = $table->grand_total;
            $order->total = $table->total;
            $order->net_amount = $table->net_amount;
            $order->receive_amount = $request->receive_amount;
            $order->created_by_id = $request->user()->id;
            $order->updated_by_id = $request->user()->id;
            if ($order->save()) {
                foreach ($table->order_detail_temps as $item) {
                    $order_detail = new OrderDetail();
                    $order_detail->order_id = $order->id;
                    $order_detail->product_id = $item->product_id;
                    $order_detail->description = $item->description;
                    $order_detail->qty = $item->qty;
                    $order_detail->unit_price = $item->unit_price;
                    $order_detail->product_category_id = $item->product_category_id;
                    $order_detail->discount = $item->discount;
                    $order_detail->created_by_id = $request->user()->id;
                    $order_detail->updated_by_id = $request->user()->id;
                    $order_detail->save();
                }
                OrderDetailTemp::where('table_id', $request->table_id)->delete();
                $table->invoice_no = '';
                $table->discount = 0;
                $table->total_discount = 0;
                $table->grand_total = 0;
                $table->total = 0;
                $table->net_amount = 0;
                $table->status = 2;
                $table->save();
            }
            DB::commit();
            return response()->json([
                'success' => true,
                'content' => view('cashier.print_receipt', compact('order'))->render()
            ]);
        } catch (Exception $ex) {
            DB::rollBack();
            abort(500, $ex->getMessage());
        }
    }
}

Setup View to Display Laravel jQuery POS Cart System

Design a clean and responsive Blade view to display the cart in real time. In your Laravel POS with jQuery system, this view updates automatically through Ajax responses—making it easy for users to see changes in the cart without any page refresh.

Replace the contents of resources/views/layout/cashier.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link rel="icon" type="image/png" href="images/favicon.png">
    <title>{{ env('APP_NAME') }}</title>
    <!-- Google Fonts -->
    <link href="https://fonts.gstatic.com" rel="preconnect">
    <link
        href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i"
        rel="stylesheet">
    @vite('resources/js/cashier.js')
</head>

<body style="display: none">
    <!-- Error Modal -->
    <div class="fade modal" tabindex="-1" id="errorModal" style="z-index: 5000;">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header py-2 bg-danger text-light">
                    <h5 class="modal-title">ERROR</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <p class="fs-5"></p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">OK</button>
                </div>
            </div>
        </div>
    </div>
    <!-- Success Modal -->
    <div class="fade modal" tabindex="-1" id="successModal">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header py-2 bg-success text-light">
                    <h5 class="modal-title">SUCCESS</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <p class="fs-5"></p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Ok</button>
                </div>
            </div>
        </div>
    </div>
    <!-- Delete Modal -->
    <div class="fade modal" tabindex="-1" id="confirmDelete">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header py-2 bg-danger text-light">
                    <h5 class="modal-title">Confirm Delete</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <p class="fs-5">Are you sure want to delete?</p>
                </div>
                <div class="modal-footer">
                    <form id="deleteForm" method="post" style="padding-bottom: 0px;margin-bottom: 0px;">
                        @method('DELETE')
                        @csrf
                        <input type="hidden" name="delete_id" id="delete_id" />
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                        <button type="submit" class="btn btn-danger" data-bs-dismiss="modal">Delete</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <!-- Form Modal -->
    <div class="modal fade" id="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
        data-bs-focus="false">
        <div class="modal-dialog modal-lg">
            <div class="modal-content"></div>
        </div>
    </div>
    <!-- For Bill and Receipt Printing -->
    <div id="printContent" class="d-none"></div>
    <header id="header" class="header fixed-top d-flex align-items-center">

        <div class="d-flex align-items-center justify-content-between">
            <a href="{{ url('/') }}" class="logo d-flex align-items-center">
                <img src="images/logo.png" alt="">
            </a>
        </div>

        <nav class="header-nav ms-auto">
            <ul class="d-flex align-items-center">
                <li class="d-none d-md-inline-block form-inline ms-auto nav-item dropdown me-5">
                    <i class="bi bi-alarm-fill text-secondary pe-2"></i>
                    <span class="text-secondary">{{ date('d-M-Y H:i:s') }}</span>
                </li>
                <li class="nav-item dropdown pe-3">
                    <a class="nav-link nav-profile d-flex align-items-center pe-0" href="#"
                        data-bs-toggle="dropdown">
                        <i class="bi bi-person-fill" style="font-size: 35px;"></i>
                        <span
                            class="d-none d-md-block dropdown-toggle ps-2">{{ ucwords(request()->user()?->username) }}</span>
                    </a>
                    <ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
                        <li>
                            <button class="dropdown-item d-flex align-items-center"
                                onclick="ajaxPopup(`{{ url('user/change-password') }}`,false)">
                                <i class="bi bi-shield-lock"></i>
                                <span>Change Password</span>
                            </button>
                        </li>
                        <li>
                            <hr class="dropdown-divider">
                        </li>
                        <li>
                            <form method="post" action="{{ url('./user/logout') }}">
                                @csrf
                                <button type="submit" class="dropdown-item d-flex align-items-center">
                                    <i class="bi bi-box-arrow-right"></i>
                                    <span>Sign Out</span>
                                </button>
                            </form>
                        </li>
                    </ul>
                </li>
            </ul>
        </nav>

    </header>

    <main id="main" class="main ms-0 pt-3">
        <div class="row">
            <div class="col-xl-7 col-lg-6">
                <div class="card mb-0" style="height: 88vh">
                    <ul class="nav nav-tabs nav-fill">
                        <li class="nav-item">
                            <span class="nav-link menu-item active" style="cursor: pointer" data-category="0"
                                onclick="getProduct(event)">ALL</span>
                        </li>
                        @foreach ($product_categories as $category)
                        <li class="nav-item">
                            <span class="nav-link menu-item" style="cursor: pointer"
                                data-category="{{ $category->id }}"
                                onclick="getProduct(event)">{{ $category->name }}</span>
                        </li>
                        @endforeach
                        <li class="nav-item">
                            <span class="nav-link menu-item" style="padding: 5px; background: whitesmoke !important">
                                <div class="input-group">
                                    <input type="text" class="form-control" placeholder="Search..."
                                        onchange="getProduct()" id="search_product" style="background: lightyellow">
                                    <button class="btn btn-success" type="button"
                                        onclick="getProduct()">Search</button>
                                </div>
                            </span>
                        </li>
                    </ul>
                    <div class="card-body p-1" style="overflow-y: scroll">
                        <div class="row row-cols-2 row-cols-sm-3 row-cols-lg-4 row-cols-xl-5 g-1" id="productList">

                        </div>
                    </div>
                </div>
            </div>
            <div class="col-xl-5 col-lg-6">
                <div class="card mb-0" style="height: 88vh" id="orderList"> </div>
            </div>
        </div>
    </main>

    <div class="loading"></div>
</body>

</html>

Replace the contents of resources/views/cashier/order_list.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<style>
    .table>:not(caption)>*>* {
        padding: 0.5rem 0.5rem !important;
    }
</style>
<div class="card-header p-2 d-none d-lg-block">
    <div class="row row-cols-4 g-1">
        <div>
            <button class="btn btn-secondary btn w-100" onclick="ajaxPopup('cashier/table/0')" title="Select Table"
                id="table_id">{{ $data ? $data->name : 'Table' }}</button>
        </div>
        <div>
            <button disabled title="Change Table" id="change_table"
                onclick="ajaxPopup('cashier/table/{{ $data?->id }}')" class="btn btn-primary w-100 btn">Change</button>
        </div>
        <div>
            <button class="btn btn-warning w-100 btn" title="Print" onclick="ajaxPrint('cashier/print-invoice',{table_id:`{{ $data?->id }}`})" @disabled(!$data || $data->order_detail_temps->count() == 0)
                >Print</button>
        </div>
        <div>
            <button class="btn btn-success w-100 btn" title="Payment" @disabled(!$data || $data->order_detail_temps->count() == 0)
                onclick="ajaxPopup('cashier/make-payment')">Payment</button>
        </div>
    </div>
</div>
<div class="card-body p-0" style="overflow-y: scroll">
    <table class="table">
        <thead>
            <tr class="table-dark">
                <th style="width: 10px" class="pb-1">
                    <input type="checkbox" id="selectAll" onchange="selectAll(event)"
                        style="width: 18px; height: 18px; margin-top: 3px" />
                </th>
                <th>Desc</th>
                <th style="width: 75px">QTY</th>
                <th class="text-end" style="width: 80px">
                    U.P ($)
                </th>
                <th class="text-end" style="width: 75px">DC(%)</th>
                <th class="text-end" style="width: 90px">
                    Total ($)
                </th>
                <th style="width: 10px"></th>
            </tr>
        </thead>
        @if ($data && $data->order_detail_temps->count() > 0)
        <tbody>
            @foreach ($data->order_detail_temps as $value)
            <tr>
                <td class="pb-0">
                    <input type="checkbox" value="{{ $value->id }}" onchange="checkItem()" class="item"
                        style="width: 18px; height: 18px; margin-top: 3px" />
                </td>
                <td> {{ $value->description }} </td>
                <td>
                    <input type="number" style="border: none; appearance: none; background: #e9ecef;"
                        class="form-control p-0 text-center" value="{{ $value->qty }}" min="1"
                        onchange="ajaxSubmit('cashier/update-order-qty',{id:`{{ $value->id }}`,qty: this.value})" />
                </td>
                <td class="text-end">
                    {{ number_format($value->unit_price, 2) }}
                </td>
                <td class="text-end">
                    <input type="number" style="border: none; appearance: none;background: #e9ecef;"
                        class="form-control p-0 text-center w-100" value="{{ $value->discount }}" min="0"
                        max="100"
                        onchange="ajaxSubmit('cashier/update-detail-discount',{id:`{{ $value->id }}`,discount: this.value})" />
                </td>
                <td class="text-end">
                    {{ number_format($value->unit_price * $value->qty * (1 - $value->discount / 100), 2) }}
                </td>
                <td>
                    <i class="bi bi-trash" style="color: red; cursor: pointer"
                        data-record-url="{{ url('cashier/delete-order-product') }}"
                        data-record-id="{{ $value->id }}" title="Delete" data-bs-toggle="modal"
                        data-bs-target="#confirmDelete"></i>
                </td>
            </tr>
            @endforeach
        </tbody>
        @endif
    </table>
</div>
@if ($data && $data->order_detail_temps->count() > 0)
<div class="card-footer p-1 text-dark" style="background: whitesmoke">
    <table class="table mb-0" style="background: whitesmoke">
        <tbody>
            <tr>
                <td class="text-end" style="width: 50px">Discount (%) :</td>
                <td style="width: 100px">
                    <input type="number"
                        style="border: none; appearance: none;background: #e9ecef;;max-width: 50px;"
                        class="form-control p-0 text-center w-100" value="{{ $data->discount }}" min="0"
                        max="100" onchange="ajaxSubmit('cashier/update-discount',{discount: this.value})" />
                </td>
                <th class="text-end" style="width: 100px">Total ($) :</th>
                <th class="text-end text-danger" style="width: 50px;">
                    {{ number_format($data->net_amount,2) }}
                </th>
            </tr>
        </tbody>
    </table>
</div>
@endif

Replace the contents of resources/views/cashier/table_list.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<div class="modal-header py-2 text-bg-secondary">
    <h5 class="modal-title" style="font-weight: bold">Select Table</h5>
</div>
<div class="modal-body">
    <div class="row row-cols-3 row-cols-sm-4 row-cols-xl-5 g-2">
        @foreach ($list as $value)
        <div>
            <div class="p-3 fs-2 text-center fw-bold w-100 {{ $value->status == 1 ? 'text-bg-danger' : ($value->status == 2 ? 'text-bg-secondary' : 'text-bg-success') }}"
                style="cursor: pointer" onclick="selectTable(`{{ $value->id }}`, `{{ $old_table_id }}`)">
                {{ $value->name }}
            </div>
        </div>
        @endforeach
    </div>
</div>
<div class="modal-footer">
    <div class="px-2 py-1 text-bg-secondary" style="text-align: right">Free</div>
    <div class="px-2 py-1 text-bg-danger" style="text-align: right">Busy</div>
    <div class="px-2 py-1 text-bg-success" style="text-align: right">Printed</div>
</div>

Replace the contents of resources/views/cashier/product_list.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
@foreach ($list as $data)
<div class="col" style="cursor: pointer" onclick="ajaxSubmit('cashier/add-to-order', { id: `{{$data->id}}` })">
    <div class="card h-100 mb-0" style="background: white">
        <div class="card-img-top"
            style="background: url({{ url($data->image ? 'storage/' . $data->image : 'images/default.png') }}) no-repeat center; height:80px">
        </div>
        <div class="card-body" style="font-size: 14px; padding: 3px">
            <p class="card-text text-center mb-1">{{ $data->name }}</p>
            <p class="card-text text-center mb-1" style="color: red">
                ${{ number_format($data->unit_price, 2) }}
            </p>
        </div>
    </div>
</div>
@endforeach

Replace the contents of resources/views/cashier/print_invoice.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<div style="text-align: center;">
    <img src="{{url('./images/sourkea.png')}}" height="100px" width="250px" />>
    <i style="font-size: 11px; display: block">
        Address: #111, Steet 999, Somroung Teav, Sen Sok, Phnom Penh. Tel: 089456111, 098456111
    </i>
    <h1 style="padding: 0px; margin: 0px; font-size: 30px">Invoice</h1>
</div>
<hr style="margin-top: 0px; padding-top: 0px" />
<table style="width: 100%; font-size: 12px">
    <thead>
        <tr>
            <td width="80px" style="text-align: right">Table No:</td>
            <td style="text-align: left">{{ $data->name }}</td>
            <td width="80px" style="text-align: right">Invoice #:</td>
            <td style="text-align: left">{{ $data->invoice_no }}</td>
        </tr>
        <tr>
            <td style="width: 60px; text-align: right">Cashier:</td>
            <td style="text-align: left; width: 100px">{{$data->createdBy?->username}}</td>
            <td style="width: 60px; text-align: right">Date:</td>
            <td style="text-align: left; width: 100px">{{ date('d-M-Y H:i:s', strtotime($data->created_at)) }}</td>
        </tr>
    </thead>
</table>
<table style="width: 100%; margin-top: 10px" border="0" cellspacing="0" cellpadding="2px">
    <thead>
        <tr style="background: darkgray">
            <th width="20px">No</th>
            <th style="text-align: left">Description</th>
            <th style="width: 8%; text-align: center">Qty</th>
            <th style="width: 16%; text-align: right">U.P ($)</th>
            <th style="width: 12%; text-align: right">Disc (%)</th>
            <th style="width: 18%; text-align: right">Total ($)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach ($data->order_detail_temps as $index=>$value)
        <tr style="font-size: 11px">
            <td align="center">{{ $index + 1 }}</td>
            <td align="left">{{ $value->description }}</td>
            <td align="center">{{ $value->qty }}</td>
            <td align="right">{{ number_format($value->unit_price,2) }}</td>
            <td align="right">{{ $value->discount }}</td>
            <td align="right">
                {{ number_format($value->unit_price * $value->qty * (1 - $value->discount / 100),2) }}
            </td>
        </tr>
        @endforeach
    </tbody>
</table>
<hr />
<table style="font-size: 14px; width: 100%;">
    <tbody>
        @if($data->discount > 0)
        <tr>
            <td style="text-align: right">
                Discount ({{ $data->discount }}%) :
            </td>
            <td style="text-align: right;">
                {{ number_format($data->total * $data->discount / 100,2) }}
            </td>
        </tr>
        @endif
        <tr>
            <th style="text-align: right">Total Amount ($) :</th>
            <th style="text-align: right; width: 100px;">{{ number_format($data->net_amount,2) }}</th>
        </tr>
    </tbody>
</table>

Replace the contents of resources/views/cashier/make_payment.blade.php with the code below. If the file doesn’t exist yet, please create it.


<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<div class="modal-header py-2">
     <h5 class="modal-title" style="font-weight: bold">Make Payment</h5>
 </div>
 <div class="modal-body p-2" style="background: white">
     <table style="width: 100%" cellspacing="0" cellpadding="2px" class="table table-striped mb-0">
         <thead>
             <tr class="table-dark">
                 <th width="20px">No</th>
                 <th style="text-align: left">Description</th>
                 <th style="width: 8%; text-align: center">Qty</th>
                 <th style="width: 16%; text-align: right">U.P($)</th>
                 <th style="width: 15%; text-align: right">Disc(%)</th>
                 <th style="width: 18%; text-align: right">Total($)</th>
             </tr>
         </thead>
         <tbody>
             @foreach ($data->order_detail_temps as $index=>$value)
             <tr>
                 <td align="center">{{ $index + 1 }}</td>
                 <td align="left">{{ $value->description }}</td>
                 <td align="center">{{ $value->qty }}</td>
                 <td align="right">{{ number_format($value->unit_price,2) }}</td>
                 <td align="center">{{ $value->discount }}</td>
                 <td align="right">{{ number_format($value->unit_price * $value->qty * (1 - $value->discount / 100),2)}}</td>
             </tr>
             @endforeach
         </tbody>
     </table>
     <table class="mt-2" style="font-size: 14px; width: 100%;" cellpadding="5px">
         <tbody>
             @if($data->discount > 0)
             <tr>
                 <td style="text-align: right">
                     Discount ({{ $data->discount }}%) :
                 </td>
                 <td style="text-align: right;">
                     {{ number_format(($data->total * $data->discount) / 100,2) }}
                 </td>
             </tr>
             @endif
             <tr>
                 <th style="text-align: right">Total ($) :</th>
                 <th style="text-align: right; width: 100px;">{{ number_format($data->total * (1 - $data->discount/100),2) }}</th>
             </tr>
         </tbody>
     </table>
 </div>
 <div class="modal-footer pt-1" style="display: block">
     <div class="row">
         <div class="col-md-8">
             <form method="POST" id="paymentForm" action="{{ url('cashier/make-payment') }}">
                 @csrf
                 <input type="hidden" name="table_id" value="{{$data->id}}" />
                 <label class="form-label required" for="autofocus">Receive Amount</label>
                 <span id="receive_amount_error" class="col-md-12 text-danger"></span>
                 <div class="row">
                     <div class="col-md-8">
                         <div class="input-group mb-1">
                             <span class="input-group-text">$</span>
                             <input type="text" class="form-control" id="autofocus" name="receive_amount" />
                             <div class="input-group-append">
                                 <button class="btn btn-success" style="border-radius: 0px" type="button"
                                     onclick="$('input[name=receive_amount]').val(`{{number_format($data->total * (1 - $data->discount/100),2)}}`)">
                                     <i class="bi bi-check-lg"></i>
                                 </button>
                             </div>
                         </div>
                     </div>
                 </div>
             </form>
         </div>
         <div class="col-md-4">
             <div class="text-end align-text-bottom pt-lg-4">
                 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                     Cancel</button> 
                 <button type="submit" class="btn btn-primary" form="paymentForm">
                     Confirm
                 </button>
             </div>
         </div>
     </div>
 </div>

Replace the contents of resources/views/cashier/print_receipt.blade.php with the code below. If the file doesn’t exist yet, please create it.

<!-- Laravel jQuery POS cart system @ https://laravelcenter.com -->
<div style="text-align: center;">
    <img src="{{url('./images/sourkea.png')}}" height="100px" width="250px" />
    <i style="font-size: 11px; display: block">
        Address: #111, Steet 999, Somroung Teav, Sen Sok, Phnom Penh. Tel: 089456111, 098456111
    </i>
    <h1 style="padding: 0px; margin: 0px; font-size: 30px">Receipt</h1>
</div>
<hr style="margin-top: 0px; padding-top: 0px" />
<table style="width: 100%; font-size: 12px">
    <thead>
        <tr>
            <td width="80px" style="text-align: right">Table No:</td>
            <td style="text-align: left">{{ $order->table?->name }}</td>
            <td width="80px" style="text-align: right">Invoice #:</td>
            <td style="text-align: left">{{ $order->invoice_no }}</td>
        </tr>
        <tr>
            <td style="width: 60px; text-align: right">Cashier:</td>
            <td style="text-align: left; width: 100px">{{$order->createdBy?->username}}</td>
            <td style="width: 60px; text-align: right">Date:</td>
            <td style="text-align: left; width: 100px">{{ date('d-M-Y H:i:s', strtotime($order->created_at)) }}</td>
        </tr>
    </thead>
</table>
<table style="width: 100%; margin-top: 10px" border="0" cellspacing="0" cellpadding="2px">
    <thead>
        <tr style="background: darkgray">
            <th width="20px">No</th>
            <th style="text-align: left">Description</th>
            <th style="width: 8%; text-align: center">Qty</th>
            <th style="width: 16%; text-align: right">U.P ($)</th>
            <th style="width: 12%; text-align: right">Disc (%)</th>
            <th style="width: 18%; text-align: right">Total ($)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach ($order->order_details as $index=>$value)
        <tr style="font-size: 11px">
            <td align="center">{{ $index + 1 }}</td>
            <td align="left">{{ $value->description }}</td>
            <td align="center">{{ $value->qty }}</td>
            <td align="right">{{ number_format($value->unit_price,2) }}</td>
            <td align="right">{{ $value->discount }}</td>
            <td align="right"> {{ number_format($value->unit_price * $value->qty * (1 - $value->discount / 100),2) }} </td>
        </tr>
        @endforeach
    </tbody>
</table>
<hr />
<table style="font-size: 14px; width: 100%;">
    <tbody>
        @if($order->discount > 0)
        <tr>
            <td style="text-align: right">
                Discount ({{ $order->discount }}%) :
            </td>
            <td style="text-align: right;">
                {{ number_format($order->total * $order->discount /100,2) }}
            </td>
        </tr>
        @endif
        <tr>
            <th style="text-align: right">Total Amount ($) :</th>
            <th style="text-align: right; width: 100px;">{{ number_format($order->net_amount,2) }}</th>
        </tr>
        <tr>
            <td style="text-align: right">Receive Amount ($) :</td>
            <td style="text-align: right; width: 100px;">{{ number_format($order->receive_amount,2) }}</td>
        </tr>
        <tr>
            <td style="text-align: right">Change Amount ($) :</td>
            <td style="text-align: right; width: 100px;">{{ number_format($order->receive_amount - $order->net_amount,2) }}</td>
        </tr>
    </tbody>
</table>
<hr />
<div style="text-align: center">
    <i style="font-size: 12px">Thank you, see you again!</i><br />
</div>

Setup Route for Laravel jQuery POS Cart System Actions

The final piece of your Laravel jQuery POS Cart System cart system is registering routes to handle the Ajax endpoints. These POST routes connect the frontend jQuery functions to your Laravel controller methods, completing the interactive workflow between UI and server.

Replace the contents of routes/web.php with the code below.

<?php
// Laravel jQuery POS cart system @ https://laravelcenter.com

use App\Http\Controllers\BalanceAdjustmentController;
use App\Http\Controllers\CashierController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProductCategoryController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\TableController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::middleware('guest')->match(['get', 'post'], '/login', [UserController::class, 'login'])->name('login');

Route::middleware('auth')->group(function () {
    Route::post('/user/logout', [UserController::class, 'logout']);

    Route::get('/', function () {
        if (Auth::user()->role == 'cashier') {
            return redirect('/cashier');
        } else
            return view('layout.admin');
    });

    Route::get('dashboard', DashboardController::class);
    // User
    Route::prefix('user')->controller(UserController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('form/{id?}', 'form');
        Route::match(['post', 'put'], 'submit', 'submit');
        Route::delete('delete', 'delete');
    });
    // Table
    Route::prefix('table')->controller(TableController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('form/{id?}',  'form');
        Route::match(['post', 'put'], 'submit', 'submit');
        Route::delete('delete', 'delete');
    });

    // Product
    Route::prefix('product')->controller(ProductController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('form/{id?}',  'form');
        Route::match(['post', 'put'], 'submit', 'submit');
        Route::delete('delete', 'delete');
    });

    // Product Category
    Route::prefix('product-category')->controller(ProductCategoryController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('form/{id?}',  'form');
        Route::match(['post', 'put'], 'submit', 'submit');
        Route::delete('delete', 'delete');
    });

    // Balance Adjustment
    Route::prefix('balance-adjustment')->controller(BalanceAdjustmentController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('form/{id?}',  'form');
        Route::match(['post', 'put'], 'submit', 'submit');
        Route::delete('delete', 'delete');
    });

    // Cashier
    Route::prefix('cashier')->controller(CashierController::class)->group(function () {
        Route::get('/', 'index');
        Route::get('product/{category}', 'product');
        Route::get('table/{id}',  'table');
        Route::post('select-table', 'selectTable');
        Route::post('update-order-qty', 'updateOrderQty');
        Route::post('update-detail-discount', 'updateDetailDiscount');
        Route::delete('delete-order-product', 'deleteOrderProduct');
        Route::post('update-discount', 'updateDiscount');
        Route::post('add-to-order', 'addToOrder');
        Route::post('print-invoice', 'printInvoice');
        Route::match(['post', 'get'], 'make-payment', 'makePayment');
    });
});
Route::fallback(function () {
    return view('404');
});

Update Vite Config for Laravel jQuery POS Cart System Assets

Replace the contents of vite.config.js with the code below

// Laravel jQuery POS Cart System @ https://laravelcenter.com
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.js', 'resources/js/cashier.js'],
            refresh: true,
        }),
    ],
});

You can always find the latest documentation and features on the official Laravel website.

Compile Assets for Laravel jQuery POS Cart System Integration

Run the following command to compile your assets:

npm run dev

Use <strong>npm run build</strong> for production.

Once compiled, visit:

http://laravel-jquery-pos

Laravel jQuery POS Cart System Setup Complete

Fantastic job! 🎉 You’ve just implemented the dynamic cart system for your Laravel POS with jQuery project using jQuery and Ajax. With real-time add-to-cart, quantity updates, and item removal, your POS now behaves like a modern, fast, and responsive checkout solution.

Your system is now capable of managing live cart updates without page reloads—making it perfect for high-speed sales environments like retail counters or restaurants.

In the next tutorial, we’ll focus on role-based access control. You’ll learn how to separate permissions and features between admins and cashiers, ensuring better security and smoother operations within your Laravel POS with jQuery application.

👉 Continue to Part 6: Role-Based Access – Admin vs. Cashier

Laravel jQuery POS Tutorial for Beginners Series

Leave a Reply

Your email address will not be published. Required fields are marked *