Laravel Vue.js POS CRUD

Laravel Vue.js POS โ€“ Part 4/7: Navigation & CRUD Operations

Introduction

Welcome to Part 4 of our Laravel Vue.js POS Tutorial Series! In this section, weโ€™ll dive into Laravel Vue.js POS CRUD functionality. Youโ€™ll learn how to build a responsive navigation system with Vue Router and implement full CRUD (Create, Read, Update, Delete) operations using Vue 3, Axios, and Laravel API. This guide will help you create reusable Vue components and set up seamless data interactions between frontend and backendโ€”perfect for powering up your POS system.

By the end of this tutorial, your project will support seamless screen switching and fully functional Create, Read, Update, and Delete operationsโ€”laying the foundation for day-to-day POS tasks.

In this part of the tutorial, weโ€™ll focus on building CRUD functionality for the key data used in our POS system. Youโ€™ll learn how to manage the following:

  • Tables (for dine-in orders)
  • Product Categories
  • Products
  • Balance Adjustments
  • System Users

Each of these will include Create, Read, Update, and Delete operations using Laravel and Ajaxโ€”making your POS system dynamic and fully manageable from the admin interface.

Setup Model for Laravel

Define Eloquent models for your system dataโ€”such as Product, Category, Table, and System User. These models handle database interactions and define the structure of each data entity. This is the first backend step in building a dynamic Laravel Vue.js POS CRUD system.

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

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Models;

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

class Table extends Model
{
    use SoftDeletes;
}

Replace the contents of app/Models/ProductCategory.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Models;

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

class ProductCategory extends Model
{
    use SoftDeletes;
}

Replace the contents of app/Models/Product.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Models;

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

class Product extends Model
{
    use SoftDeletes;
}

Replace the contents of app/Models/BalanceAdjustment.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Models;

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

class BalanceAdjustment extends Model
{
    use SoftDeletes;
}

Update the existing file app/Models/User.php and add SoftDeletes in.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
     use HasFactory, Notifiable, SoftDeletes;
}

Setup Controller to Handle CRUD Requests

Build a dedicated Laravel controller to manage Create, Read, Update, and Delete (CRUD) logic for your POS app. It handles all API requests in the Laravel Vue.js POS CRUD workflow.

Replace the contents of app/Http/Controllers/TableController.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\Table;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class TableController extends Controller
{
    public function list(Request $request)
    {
        // get param value
        $name = $request->name;
        $status = $request->status ?? 0;
        $sortBy = $request->sortBy ?? 'created_at';
        $orderBy = $request->orderBy ?? 'desc';
        try {
            $data = Table::select('id', 'name', 'status', 'order', 'created_at')
                ->when($name, function ($query) use ($name) {
                    $query->where('name', 'like', '%' . $name . '%');
                })
                ->when($status > 0, function ($query) use ($status) {
                    $query->where('status',  $status);
                })
                ->orderBy($sortBy, $orderBy)
                ->paginate(50);
            $response['success'] = true;
            $response['data'] = $data;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }

        return response()->json($response);
    }


    public function save(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|unique:tables,name,' . $request->id,
            'status' => 'required',
            'order' => 'nullable|numeric',
        ]);
        if ($validator->fails()) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => $validator->errors()
                ]
            );
        }
        try {
            // DB::beginTransaction();
            if ($request->id > 0) {
                $data = Table::find($request->id);
            } else {
                $data = new Table();
                $data->created_by_id = $request->user()->id;
            }

            $data->updated_by_id = $request->user()->id;
            $data->name = $request->name;
            $data->status = $request->status;
            $data->order = $request->order;
            $data->save();
            $response['success'] = true;
            $response['data'] = null;
            // DB::commit();
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }

    public function edit(Request $request)
    {
        return response()->json(Table::select('id', 'name', 'status', 'order')->findOrFail($request->id));
    }

    public function delete(Request $request)
    {
        $data = Table::findOrFail($request->id);
        $data->deleted_id = $request->user()->id;
        $data->delete();
        return response()->json();
    }
}

Replace the contents of app/Http/Controllers/ProductCategoryController.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\ProductCategory;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ProductCategoryController extends Controller
{
    public function list(Request $request)
    {
        // get param value
        $name = $request->name;
        $sortBy = $request->sortBy ?? 'created_at';
        $orderBy = $request->orderBy ?? 'desc';
        try {
            $data = ProductCategory::select('id', 'name', 'order', 'created_at')
                ->when($name, function ($query) use ($name) {
                    $query->where('name', 'like', '%' . $name . '%');
                })
                ->orderBy($sortBy, $orderBy)
                ->paginate(50);
            $response['success'] = true;
            $response['data'] = $data;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }

        return response()->json($response);
    }


    public function save(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|unique:tables,name,' . $request->id,
            'order' => 'nullable|numeric',
        ]);
        if ($validator->fails()) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => $validator->errors()
                ]
            );
        }
        try {
            // DB::beginTransaction();
            if ($request->id > 0) {
                $data = ProductCategory::find($request->id);
            } else {
                $data = new ProductCategory();
                $data->created_by_id = $request->user()->id;
            }

            $data->updated_by_id = $request->user()->id;
            $data->name = $request->name;
            $data->order = $request->order;
            $data->save();
            $response['success'] = true;
            $response['data'] = null;
            // DB::commit();
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }

    public function edit(Request $request)
    {
        return response()->json(ProductCategory::select('id', 'name', 'order')->findOrFail($request->id));
    }

    public function delete(Request $request)
    {
        $data = ProductCategory::findOrFail($request->id);
        $data->deleted_id = $request->user()->id;
        $data->delete();
        return response()->json();
    }
}

Replace the contents of app/Http/Controllers/ProductController.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\Product;
use App\Models\ProductCategory;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class ProductController extends Controller
{
    public function list(Request $request)
    {
        // get param value
        $name = $request->name;
        $product_category_id = $request->product_category_id ?? 0;
        $sortBy = $request->sortBy ?? 'created_at';
        $orderBy = $request->orderBy ?? 'desc';
        try {
            // product list
            $data = Product::join('product_categories', 'product_categories.id', '=', 'products.product_category_id')
                ->when($name, function ($query) use ($name) {
                    $query->where('products.name', 'like', '%' . $name . '%');
                })
                ->when($product_category_id > 0, function ($query) use ($product_category_id) {
                    $query->where('products.product_category_id', '=', $product_category_id);
                })
                ->select('products.id', 'products.name', 'products.image', 'products.unit_price', 'products.created_at', 'product_categories.name AS category_name', 'products.order')
                ->orderBy($sortBy, $orderBy)
                ->paginate(50);

            $response['success'] = true;
            $response['data'] = $data;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }

        return response()->json($response);
    }


    public function save(Request $request)
    {
        $validator = Validator::make(
            $request->all(),
            [
                'name' => 'required|unique:tables,name,' . $request->id,
                'image' => 'nullable|image|mimes:png,jpg,jpeg|max:1024',
                'product_category_id' => 'required',
                'unit_price' => 'required|numeric',
                'order' => 'nullable|numeric',
            ],
            [],
            [
                'product_category_id' => 'product category',
            ]
        );
        if ($validator->fails()) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => $validator->errors()
                ]
            );
        }
        try {
            // DB::beginTransaction();
            if ($request->id > 0) {
                $data = Product::find($request->id);
            } else {
                $data = new Product();
                $data->created_by_id = $request->user()->id;
            }

            $data->updated_by_id = $request->user()->id;
            $data->name = $request->name;
            $data->product_category_id = $request->product_category_id;
            $data->unit_price = $request->unit_price;
            $data->name = $request->name;
            $data->order = $request->order;

            // delete uploaded file
            if ($request->is_deleted_image == 1 && $request->id > 0) {
                if (Storage::disk('public')->exists($data->image)) {
                    Storage::disk('public')->delete($data->image);
                }
                $data->image = '';
            }
            // upload file
            else if ($request->hasFile('image')) {
                if ($data->image && Storage::disk('public')->exists($data->image)) {
                    Storage::disk('public')->delete($data->image);
                }
                $data->image = Storage::disk('public')->put('product', $request->image);
            }

            $data->save();
            $response['success'] = true;
            $response['data'] = null;
            // DB::commit();
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }

    public function edit(Request $request)
    {
        return response()->json(Product::select('id', 'name', 'product_category_id', 'unit_price', 'image', 'order')->findOrFail($request->id));
    }

    public function delete(Request $request)
    {
        $data = Product::findOrFail($request->id);
        $data->deleted_id = $request->user()->id;
        $data->delete();
        // delete uploaded file
        if ($data->image && Storage::disk('public')->exists($data->image)) {
            Storage::disk('public')->delete($data->image);
        }
        return response()->json();
    }

    public function categoryList()
    {
        return response()->json(ProductCategory::select('id', 'name')->orderBy('name')->get());
    }
}

Replace the contents of app/Http/Controllers/BalanceAdjustmentController.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\BalanceAdjustment;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class BalanceAdjustmentController extends Controller
{
    public function list(Request $request)
    {
        // get param value
        $remark = $request->remark;
        $type_id = $request->type_id ?? 0;
        $sortBy = $request->sortBy ?? 'created_at';
        $orderBy = $request->orderBy ?? 'desc';
        $from_date = null;
        $to_date = null;
        if ($request->from_date)
            $from_date = date('Y-m-d 00:00:00', strtotime($request->from_date));
        if ($request->to_date)
            $to_date = date('Y-m-d 23:59:59', strtotime($request->to_date));
        try {
            $data = BalanceAdjustment::select('id', 'amount', 'remark', 'type_id', 'adjustment_date', 'created_at')
                ->when($remark, function ($query) use ($remark) {
                    $query->where('remark', 'like', '%' . $remark . '%');
                })
                ->when($type_id > 0, function ($query) use ($type_id) {
                    $query->where('type_id',  $type_id);
                })
                ->when($from_date, function ($query) use ($from_date) {
                    $query->where('created_at', '>=', $from_date);
                })
                ->when($to_date, function ($query) use ($to_date) {
                    $query->where('created_at', '<=', $to_date);
                })
                ->orderBy($sortBy, $orderBy)
                ->paginate(50);

            $response['success'] = true;
            $response['data'] = $data;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }

        return response()->json($response);
    }


    public function save(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'amount' => 'required|numeric',
            'type_id' => 'required',
            'adjustment_date' => 'required',
            'remark' => 'required'
        ]);
        if ($validator->fails()) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => $validator->errors()
                ]
            );
        }
        try {
            // DB::beginTransaction();
            if ($request->id > 0) {
                $data = BalanceAdjustment::find($request->id);
            } else {
                $data = new BalanceAdjustment();
                $data->created_by_id = $request->user()->id;
            }

            $data->updated_by_id = $request->user()->id;
            $data->amount = $request->amount;
            $data->type_id = $request->type_id;
            $data->adjustment_date = date('Y-m-d', strtotime($request->adjustment_date));
            $data->remark = $request->remark;
            $data->save();
            $response['success'] = true;
            $response['data'] = null;
            // DB::commit();
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }

    public function edit(Request $request)
    {
        return response()->json(BalanceAdjustment::select('id', 'amount', 'type_id', 'remark', 'adjustment_date')->findOrFail($request->id));
    }

    public function delete(Request $request)
    {
        $data = BalanceAdjustment::findOrFail($request->id);
        $data->deleted_id = $request->user()->id;
        $data->delete();
        return response()->json();
    }
}

Replace the contents of app/Http/Controllers/UserController.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
namespace App\Http\Controllers;

use App\Models\User;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class UserController extends Controller
{
    public function list(Request $request)
    {
        // get param value
        $username = $request->username;
        $role = $request->role ?? 0;
        $sortBy = $request->sortBy ?? 'created_at';
        $orderBy = $request->orderBy ?? 'desc';
        try {
            $data = User::select('id', 'username', 'role', 'active', 'created_at')
                ->when($username, function ($query) use ($username) {
                    $query->where('username', 'like', '%' . $username . '%');
                })
                ->when($role, function ($query) use ($role) {
                    $query->where('role',  $role);
                })
                ->orderBy($sortBy, $orderBy)
                ->paginate(50);
            $response['success'] = true;
            $response['data'] = $data;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }

        return response()->json($response);
    }


    public function save(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'username' => 'required|alpha_num|unique:users,username,' . $request->id,
            'role' => 'required',
            'password' => [$request->id > 0 ? 'nullable' : 'required', 'confirmed'],
        ]);
        if ($validator->fails()) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => $validator->errors()
                ]
            );
        }
        try {
            if ($request->id > 0) {
                $data = User::find($request->id);
            } else {
                $data = new User();
                $data->created_by_id = $request->user()->id;
            }

            $data->updated_by_id = $request->user()->id;
            $data->username = $request->username;
            $data->role = $request->role;
            $data->active = $request->active == 'on';
            if ($request->password)
                $data->password = bcrypt($request->password);
            $data->save();
            $response['success'] = true;
            $response['data'] = null;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }

    public function edit(Request $request)
    {
        return response()->json(User::select('id', 'username', 'active', 'role')->findOrFail($request->id));
    }

    public function delete(Request $request)
    {
        $data = User::findOrFail($request->id);
        $data->deleted_id = $request->user()->id;
        $data->delete();
        return response()->json();
    }
}

Setup Vue Component

Design Vue components to handle user interactions and render the POS interface. These components are essential for the frontend of your Laravel Vue.js POS CRUD system.

Replace the contents of resources/js/component/data/Table.vue with the code below. If the file doesnโ€™t exist yet, please create it.

<template>
  <div class="vl-parent">
    <Loading v-model:active="isLoading" :is-full-page="true" color="blue" />

    <ShareModal ref="messageBox"></ShareModal>

    <!-- Form Modal -->
    <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
      data-bs-backdrop="static" data-bs-focus="false">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header py-2 bg-secondary text-light">
            <h5 class="modal-title" style="font-weight: bold">
              {{ form.id ? "Edit" : "Create" }} Table
            </h5>
          </div>
          <div class="modal-body">
            <form @submit.prevent="saveData" id="form">
              <div class="row">
                <div class="col-12 mb-3">
                  <label class="form-label required">Name</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.name }]" v-model="form.name"
                    ref="autofocus" />
                  <span v-if="errors.name" class="invalid-feedback"> {{ errors.name[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Status</label>
                  <select :class="['form-select', { 'is-invalid': errors.status }]" v-model="form.status">
                    <option v-for="(name, id) in statusList" :key="id" :value="id">
                      {{ name }}
                    </option>
                  </select>
                  <span v-if="errors.status" class="invalid-feedback"> {{ errors.status[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label">Order</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.order }]" v-model="form.order" />
                  <span v-if="errors.order" class="invalid-feedback"> {{ errors.order[0] }} </span>
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
              <i class="bi bi-x-lg"></i> Cancel
            </button>
            <button type="submit" class="btn btn-primary px-3" form="form">
              <i class="bi bi-floppy" style="padding-right: 3px;"></i> Save
            </button>
          </div>
        </div>
      </div>
    </div>

    <button type="button" class="btn btn-primary" style="float: right" @click="openModal">
      <i class="bi bi-plus-circle"></i> Add New
    </button>

    <div class="pagetitle">
      <h1>Table</h1>
    </div>
    <section class="section">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <!-- Filter -->
            <form @submit.prevent="getData(true)">
              <div class="row pt-4">
                <div class="col-md-10">
                  <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Name</label>
                      <input type="text" class="form-control" v-model="filter.name" placeholder="Search..." />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Status</label>
                      <select class="form-select" v-model="filter.status">
                        <option value="0">ALL</option>
                        <option v-for="(name, id) in statusList" :key="id" :value="id">
                          {{ name }}
                        </option>
                      </select>
                    </div>
                  </div>
                </div>
                <div class="col-md-2 align-self-end">
                  <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                  </button>
                </div>
              </div>
            </form>
            <hr class="text-secondary" />
            <!-- Data List -->
            <table class="table table-striped">
              <thead>
                <tr class="table-dark">
                  <th scope="col" width="50px">#</th>
                  <th scope="col" width="100px">
                    Action</th>
                  <th scope="col" @click="sortData('name')" style="cursor: pointer">
                    Name <i class="text-secondary"
                      :class="filter.sortBy == 'name' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('status')" style="cursor: pointer">
                    Status <i class="text-secondary"
                      :class="filter.sortBy == 'status' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th class="text-center" scope="col" @click="sortData('order')" style="cursor: pointer">
                    Order <i class="text-secondary"
                      :class="filter.sortBy == 'order' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('created_at')" style="cursor: pointer" width="200px">
                    Created Time <i class="text-secondary"
                      :class="filter.sortBy == 'created_at' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr v-if="dataList && dataList.data && dataList.data.length > 0" v-for="(d, index) in dataList.data"
                  :key="d.id">
                  <th scope="row">{{ dataList.from + index }}</th>
                  <td>
                    <i class="bi bi-trash3-fill pe-3 text-danger" role="button" @click="deleteData(d.id)"></i>
                    <i class="bi bi-pencil-square text-success" role="button" @click="editData(d.id)"></i>
                  </td>
                  <td>{{ d.name }}</td>
                  <td class="text-capitalize"
                    :class="{ 'text-danger': d.status == 1, 'text-primary': d.status == 2, 'text-success': d.status == 3 }">
                    {{ d.status == 1 ? 'Busy' : d.status == 2 ? 'Free' : 'Printed' }}
                  </td>
                  <td class="text-center">{{ d.order }}</td>
                  <td>{{ dateFormat(d.created_at) }}</td>
                </tr>
                <tr v-else>
                  <td colspan="10" class="shadow-none">
                    No record found
                  </td>
                </tr>
              </tbody>
            </table>
            <!-- Pagination -->
            <div class="d-flex justify-content-end">
              <nav v-if="dataList.links && dataList.links.length > 3">
                <ul class="pagination">
                  <li :class="['page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                    v-for="data in dataList.links">
                    <span class="page-link" style="cursor: pointer" v-html="data.label" v-if="data.url && !data.active"
                      @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                    <span class="page-link" v-html="data.label" v-else></span>
                  </li>
                </ul>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { onMounted, onUnmounted, ref } from 'vue';
import { Modal } from 'bootstrap';
import { clearForm, dateFormat, setFocus } from '../../helper.js';
import ShareModal from '../Share/Modal.vue';
import axios from 'axios';

const isLoading = ref(false);
const formModalInstance = ref(null);
const formModal = ref(null);
const messageBox = ref(null);
const autofocus = ref(null);
const statusList = ref({ '1': 'Busy', '2': 'Free', '3': 'Printed' });
const form = ref(
  {
    id: null,
    name: null,
    status: null,
    order: null
  }
);
const filter = ref(
  {
    name: null,
    status: 0,
    sortBy: null,
    orderBy: null,
    page: 1
  }
);
const dataList = ref([]);
const errors = ref({});
onMounted(() => {
  if (formModal.value) {
    formModalInstance.value = new Modal(formModal.value);
    formModal.value.addEventListener("shown.bs.modal", () => {
      setFocus(autofocus);
    });
    formModal.value.addEventListener("hide.bs.modal", () => {
      document.activeElement?.blur();
    });
    formModal.value.addEventListener("hidden.bs.modal", () => {
      clearForm(form.value);
      errors.value = {};
    });
  }
  getData(true);
});
onUnmounted(() => {
  if (formModalInstance.value) {
    formModalInstance.value.dispose();
  }
});

// add or create
const openModal = () => {
  isLoading.value = false;
  formModalInstance.value.show();
};

// submit form
const saveData = () => {
  isLoading.value = true;
  axios[form.value.id > 0 ? "put" : "post"]("api/table/save", form.value)
    .then((response) => {
      if (response.data.success) {
        formModalInstance.value.hide();
        messageBox.value.showModal(1);
        getData();
      } else {
        errors.value = response.data.errors;
        setFocus(autofocus);
      }
    })
    .catch((ex) => {
      console.log(ex);
      setFocus(autofocus);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// load data
const getData = (resetPge = false) => {
  isLoading.value = true;
  if (resetPge)
    filter.value.page = 1;
  axios.post("api/table/list", filter.value).then((response) => {
    if (response.data.success) {
      dataList.value = response.data.data;
    }
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// Pagination
const paginate = (page_number) => {
  filter.value.page = page_number;
  if (page_number > dataList.last_page) {
    filter.value.page = dataList.last_page;
  }
  if (page_number <= 0) {
    filter.value.page = 1;
  }
  getData();
};

// sort
const sortData = (field) => {
  if (filter.value.sortBy === field) {
    filter.value.orderBy = filter.value.orderBy == 'asc' ? 'desc' : 'asc';
  } else {
    filter.value.sortBy = field;
    filter.value.orderBy = 'asc';
  }
  getData();
};

// edit
const editData = (id) => {
  isLoading.value = true;
  axios.get("api/table/edit/" + id).then((response) => {
    Object.keys(form.value).forEach(key => {
      if (key in response.data) {
        form.value[key] = response.data[key];
      }
    });
    formModalInstance.value.show();
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// delete
const deleteData = (id) => {
  messageBox.value.showModal(4, () => {
    isLoading.value = true;
    axios.delete("api/table/delete/" + id).then(() => {
      getData(true);
    })
      .catch((ex) => {
        console.log(ex);
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
};

</script>

Replace the contents of resources/js/component/data/ProductCategory.vue with the code below. If the file doesnโ€™t exist yet, please create it.

<template>
  <div class="vl-parent">
    <Loading v-model:active="isLoading" :is-full-page="true" color="blue" />

    <ShareModal ref="messageBox"></ShareModal>

    <!-- Form Modal -->
    <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
      data-bs-backdrop="static" data-bs-focus="false">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header py-2 bg-secondary text-light">
            <h5 class="modal-title" style="font-weight: bold">
              {{ form.id ? "Edit" : "Create" }} Product Category
            </h5>
          </div>
          <div class="modal-body">
            <form @submit.prevent="saveData" id="form">
              <div class="row">
                <div class="col-12 mb-3">
                  <label class="form-label required">Name</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.name }]" v-model="form.name"
                    ref="autofocus" />
                  <span v-if="errors.name" class="invalid-feedback"> {{ errors.name[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label">Order</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.order }]" v-model="form.order" />
                  <span v-if="errors.order" class="invalid-feedback"> {{ errors.order[0] }} </span>
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
              <i class="bi bi-x-lg"></i> Cancel
            </button>
            <button type="submit" class="btn btn-primary px-3" form="form">
              <i class="bi bi-floppy" style="padding-right: 3px;"></i> Save
            </button>
          </div>
        </div>
      </div>
    </div>

    <button type="button" class="btn btn-primary" style="float: right" @click="openModal">
      <i class="bi bi-plus-circle"></i> Add New
    </button>

    <div class="pagetitle">
      <h1>Product Category</h1>
    </div>
    <section class="section">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <!-- Filter -->
            <form @submit.prevent="getData(true)">
              <div class="row pt-4">
                <div class="col-md-10">
                  <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Name</label>
                      <input type="text" class="form-control" v-model="filter.name" placeholder="Search..." />
                    </div>
                  </div>
                </div>
                <div class="col-md-2 align-self-end">
                  <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                  </button>
                </div>
              </div>
            </form>
            <hr class="text-secondary" />
            <!-- Data List -->
            <table class="table table-striped">
              <thead>
                <tr class="table-dark">
                  <th scope="col" width="50px">#</th>
                  <th scope="col" width="100px">
                    Action</th>
                  <th scope="col" @click="sortData('name')" style="cursor: pointer">
                    Name <i class="text-secondary"
                      :class="filter.sortBy == 'name' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('order')" style="cursor: pointer">
                    Order <i class="text-secondary"
                      :class="filter.sortBy == 'order' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('created_at')" style="cursor: pointer" width="200px">
                    Created Time <i class="text-secondary"
                      :class="filter.sortBy == 'created_at' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr v-if="dataList && dataList.data && dataList.data.length > 0" v-for="(d, index) in dataList.data"
                  :key="d.id">
                  <th scope="row">{{ dataList.from + index }}</th>
                  <td>
                    <i class="bi bi-trash3-fill pe-3 text-danger" role="button" @click="deleteData(d.id)"></i>
                    <i class="bi bi-pencil-square text-success" role="button" @click="editData(d.id)"></i>
                  </td>
                  <td>{{ d.name }}</td>
                  <td>{{ d.order }}</td>
                  <td>{{ dateFormat(d.created_at) }}</td>
                </tr>
                <tr v-else>
                  <td colspan="10" class="shadow-none">
                    No record found
                  </td>
                </tr>
              </tbody>
            </table>
            <!-- Pagination -->
            <div class="d-flex justify-content-end">
              <nav v-if="dataList.links && dataList.links.length > 3">
                <ul class="pagination">
                  <li :class="['page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                    v-for="data in dataList.links">
                    <span class="page-link" style="cursor: pointer" v-html="data.label" v-if="data.url && !data.active"
                      @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                    <span class="page-link" v-html="data.label" v-else></span>
                  </li>
                </ul>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { Modal } from 'bootstrap';
import { clearForm, dateFormat, setFocus } from '../../helper.js';
import ShareModal from '../Share/Modal.vue';
import axios from 'axios';

const isLoading = ref(false);
const formModalInstance = ref(null);
const formModal = ref(null);
const messageBox = ref(null);
const autofocus = ref(null);
const form = ref(
  {
    id: null,
    name: null,
    order: null
  }
);
const filter = ref(
  {
    name: null,
    sortBy: null,
    orderBy: null,
    page: 1
  }
);
const dataList = ref([]);
const errors = ref({});
onMounted(() => {
  if (formModal.value) {
    formModalInstance.value = new Modal(formModal.value);
    formModal.value.addEventListener("shown.bs.modal", () => {
      setFocus(autofocus);
    });
    formModal.value.addEventListener("hide.bs.modal", () => {
      document.activeElement?.blur();
    });
    formModal.value.addEventListener("hidden.bs.modal", () => {
      clearForm(form.value);
      errors.value = {};
    });
  }
  getData(true);
});
onUnmounted(() => {
  if (formModalInstance.value) {
    formModalInstance.value.dispose();
  }
});

// add or create
const openModal = () => {
  isLoading.value = false;
  formModalInstance.value.show();
};

// submit form
const saveData = () => {
  isLoading.value = true;
  axios[form.value.id > 0 ? "put" : "post"]("api/product-category/save", form.value)
    .then((response) => {
      if (response.data.success) {
        formModalInstance.value.hide();
        messageBox.value.showModal(1);
        getData();
      } else {
        errors.value = response.data.errors;
        setFocus(autofocus);
      }
    })
    .catch((ex) => {
      console.log(ex);
      setFocus(autofocus);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// load data
const getData = (resetPge = false) => {
  isLoading.value = true;
  if (resetPge)
    filter.value.page = 1;
  axios.post("api/product-category/list", filter.value).then((response) => {
    if (response.data.success) {
      dataList.value = response.data.data;
    }
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// Pagination
const paginate = (page_number) => {
  filter.value.page = page_number;
  if (page_number > dataList.last_page) {
    filter.value.page = dataList.last_page;
  }
  if (page_number <= 0) {
    filter.value.page = 1;
  }
  getData();
};

// sort
const sortData = (field) => {
  if (filter.value.sortBy === field) {
    filter.value.orderBy = filter.value.orderBy == 'asc' ? 'desc' : 'asc';
  } else {
    filter.value.sortBy = field;
    filter.value.orderBy = 'asc';
  }
  getData();
};

// edit
const editData = (id) => {
  isLoading.value = true;
  axios.get("api/product-category/edit/" + id).then((response) => {
    Object.keys(form.value).forEach(key => {
      if (key in response.data) {
        form.value[key] = response.data[key];
      }
    });
    formModalInstance.value.show();
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// delete
const deleteData = (id) => {
  messageBox.value.showModal(4, () => {
    isLoading.value = true;
    axios.delete("api/product-category/delete/" + id).then(() => {
      getData(true);
    })
      .catch((ex) => {
        console.log(ex);
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
};

</script>

Replace the contents of resources/js/component/data/Product.vue with the code below. If the file doesnโ€™t exist yet, please create it.

<template>
  <div class="vl-parent">
    <Loading v-model:active="isLoading" :is-full-page="true" color="blue" />
    <!-- Share Modal -->
    <ShareModal ref="messageBox"></ShareModal>

    <!-- Form Modal -->
    <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
      data-bs-backdrop="static" data-bs-focus="false">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header py-2 bg-secondary text-light">
            <h5 class="modal-title" style="font-weight: bold">
              {{ form.id ? "Edit" : "Add" }} Product
            </h5>
          </div>
          <div class="modal-body">
            <form @submit.prevent="saveData" id="form">
              <div class="row">
                <div class="col-12 mb-3">
                  <label class="form-label">Image</label>
                  <div style="position: relative; width: 40%;" :class="{ 'is-invalid': errors.image }">
                    <i class="bi bi-x-circle fs-3 m-0 p-0 text-danger"
                      style="position: absolute; right: 5px;top: -2px; cursor: pointer;" @click.stop="removeImage"></i>
                    <img :src="form.image_preview" style="width: 100%;cursor: pointer;" class="img-thumbnail"
                      @click="upload" />
                  </div>
                  <span v-if="errors.image" class="invalid-feedback"> {{ errors.image[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Name</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.name }]" v-model="form.name"
                    ref="autofocus" />
                  <span v-if="errors.name" class="invalid-feedback"> {{ errors.name[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Product Category</label>
                  <select :class="['form-select', { 'is-invalid': errors.product_category_id }]"
                    v-model="form.product_category_id" :disabled="form.processing">
                    <option v-for="(value, key) in productCategoryList" :key="key" :value="key">
                      {{ value }}
                    </option>
                  </select>
                  <span v-if="errors.product_category_id" class="invalid-feedback"> {{
                    errors.product_category_id[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Unit Price</label>
                  <div class="input-group" :class="{ 'is-invalid': errors.unit_price }">
                    <span class="input-group-text">$</span>
                    <input type="text" :class="['form-control', { 'is-invalid': errors.unit_price }]"
                      v-model="form.unit_price" :disabled="form.processing" />
                  </div>
                  <span v-if="errors.unit_price" class="invalid-feedback"> {{
                    errors.unit_price[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label">Order</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.order }]" v-model="form.order" />
                  <span v-if="errors.order" class="invalid-feedback"> {{ errors.order[0] }} </span>
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
              <i class="bi bi-x-lg"></i> Cancel
            </button>
            <button type="submit" class="btn btn-primary px-3" form="form">
              <i class="bi bi-floppy" style="padding-right: 3px;"></i> Save
            </button>
          </div>
        </div>
      </div>
    </div>

    <button type="button" class="btn btn-primary" style="float: right" @click="openModal">
      <i class="bi bi-plus-circle"></i> Add New
    </button>

    <div class="pagetitle">
      <h1>Product</h1>
    </div>

    <section class="section">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <form @submit.prevent="getData(true)">
              <div class="row pt-4">
                <div class="col-md-10">
                  <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Name</label>
                      <input type="text" class="form-control" v-model="filter.name" placeholder="Search..." />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Status</label>
                      <select class="form-select" v-model="filter.product_category_id">
                        <option value="0">ALL</option>
                        <option v-for="data in productCategoryList" :key="data.id" :value="data.id">
                          {{ data.name }}
                        </option>
                      </select>
                    </div>
                  </div>
                </div>
                <div class="col-md-2 align-self-end">
                  <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                  </button>
                </div>
              </div>
            </form>
            <hr class="text-secondary" />
            <!-- Default Product -->
            <table class="table table-striped">
              <thead>
                <tr class="table-dark">
                  <th scope="col" width="50px">#</th>
                  <th scope="col" width="100px">Action</th>
                  <th scope="col" width="100px">Image</th>
                  <th scope="col" @click="sortData('products.name')" style="cursor: pointer">
                    Name <i class="text-secondary"
                      :class="filter.sortBy == 'products.name' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('product_categories.name')" style="cursor: pointer">
                    Category <i class="text-secondary"
                      :class="filter.sortBy == 'product_categories.name' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('unit_price')" style="cursor: pointer">
                    Unit Price <i class="text-secondary"
                      :class="filter.sortBy == 'unit_price' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th class="text-center" scope="col" @click="sortData('order')" style="cursor: pointer">
                    Order <i class="text-secondary"
                      :class="filter.sortBy == 'order' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('products.created_at')" style="cursor: pointer" width="200px">
                    Created Time <i class="text-secondary"
                      :class="filter.sortBy == 'products.created_at' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr v-if="dataList && dataList.data && dataList.data.length > 0" v-for="(d, index) in dataList.data"
                  :key="d.id">
                  <th scope="row">{{ dataList.from + index }}</th>
                  <td>
                    <i class="bi bi-trash3-fill pe-3 text-danger" role="button" @click="deleteData(d.id)"></i>
                    <i class="bi bi-pencil-square text-success" role="button" @click="editData(d.id)"></i>
                  </td>
                  <td>
                    <a :href="d.image ? ('storage/' + d.image) : defaultImage" target="_blank">
                      <img :src="d.image ? ('storage/' + d.image) : defaultImage" style="height: 40px;"
                        class="img-thumbnail" />
                    </a>
                  </td>
                  <td>{{ d.name }}</td>
                  <td>{{ d.category_name }}</td>
                  <td>{{ currencyFormat(d.unit_price) }}</td>
                  <td class="text-center">{{ d.order }}</td>
                  <td>{{ dateFormat(d.created_at) }}</td>
                </tr>
                <tr v-else>
                  <td colspan="10" class="shadow-none">
                    No record found
                  </td>
                </tr>
              </tbody>
            </table>
            <!-- Pagination -->
            <div class="d-flex justify-content-end">
              <nav v-if="dataList.links && dataList.links.length > 3">
                <ul class="pagination">
                  <li :class="['page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                    v-for="data in dataList.links">
                    <span class="page-link" style="cursor: pointer" v-html="data.label" v-if="data.url && !data.active"
                      @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                    <span class="page-link" v-html="data.label" v-else></span>
                  </li>
                </ul>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>


  </div>
</template>
<script setup>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { onMounted, onUnmounted, ref } from 'vue';
import { Modal } from 'bootstrap';
import { clearForm, currencyFormat, dateFormat, setFocus } from '../../helper.js';
import ShareModal from '../Share/Modal.vue';
import axios from 'axios';

const isLoading = ref(false);
const formModalInstance = ref(null);
const formModal = ref(null);
const messageBox = ref(null);
const autofocus = ref(null);
const productCategoryList = ref([]);
const defaultImage = "images/default.png";
const form = ref(
  {
    id: null,
    name: null,
    product_category_id: null,
    unit_price: null,
    order: null,
    image: null,
    image_preview: defaultImage,
    image_remove: null
  }
);
const filter = ref(
  {
    name: null,
    product_category_id: 0,
    sortBy: null,
    orderBy: null,
    page: 1
  }
);
const dataList = ref([]);
const errors = ref({});
onMounted(() => {
  if (formModal.value) {
    formModalInstance.value = new Modal(formModal.value);
    formModal.value.addEventListener("shown.bs.modal", () => {
      setFocus(autofocus);
    });
    formModal.value.addEventListener("hide.bs.modal", () => {
      document.activeElement?.blur();
    });
    formModal.value.addEventListener("hidden.bs.modal", () => {
      clearForm(form.value);
      errors.value = {};
    });
  }
  getData(true);
  getProductCategoryList();
});
onUnmounted(() => {
  if (formModalInstance.value) {
    formModalInstance.value.dispose();
  }
});

// add or create
const openModal = () => {
  isLoading.value = false;
  form.value.image_preview = defaultImage;
  form.value.image_remove = null;
  formModalInstance.value.show();
};

// submit form
const saveData = () => {
  isLoading.value = true;
  axios.post("api/product/save", form.value, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
    .then((response) => {
      if (response.data.success) {
        formModalInstance.value.hide();
        messageBox.value.showModal(1);
        getData();
      } else {
        errors.value = response.data.errors;
        setFocus(autofocus);
      }
    })
    .catch((ex) => {
      console.log(ex);
      setFocus(autofocus);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// load data
const getData = (resetPge = false) => {
  isLoading.value = true;
  if (resetPge)
    filter.value.page = 1;
  axios.post("api/product/list", filter.value).then((response) => {
    if (response.data.success) {
      dataList.value = response.data.data;
    }
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// Pagination
const paginate = (page_number) => {
  filter.value.page = page_number;
  if (page_number > dataList.last_page) {
    filter.value.page = dataList.last_page;
  }
  if (page_number <= 0) {
    filter.value.page = 1;
  }
  getData();
};

// sort
const sortData = (field) => {
  if (filter.value.sortBy === field) {
    filter.value.orderBy = filter.value.orderBy == 'asc' ? 'desc' : 'asc';
  } else {
    filter.value.sortBy = field;
    filter.value.orderBy = 'asc';
  }
  getData();
};

// edit
const editData = (id) => {
  isLoading.value = true;
  axios.get("api/product/edit/" + id).then((response) => {
    Object.keys(form.value).forEach(key => {
      if (key in response.data) {
        form.value[key] = response.data[key];
      }
    });
    form.value.image_preview = (form.value.image ? ('storage/' + form.value.image) : defaultImage);
    form.value.image = null
    form.value.image_remove = null;
    formModalInstance.value.show();
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// delete
const deleteData = (id) => {
  messageBox.value.showModal(4, () => {
    isLoading.value = true;
    axios.delete("api/product/delete/" + id).then(() => {
      getData(true);
    })
      .catch((ex) => {
        console.log(ex);
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
};

// get product category list
const getProductCategoryList = () => {
  isLoading.value = true;
  axios.get("api/product/category-list").then((response) => {
    productCategoryList.value = response.data;
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// upload file
const upload = () => {
  let acceptFileType = ['image/png', 'image/jpg', 'image/jpeg'];
  let input = document.createElement('input');
  input.type = 'file';
  input.accept = '.png,.jpg,.jpeg';
  input.onchange = _ => {
    let file = input.files[0];
    if (!acceptFileType.includes(file.type.toLocaleLowerCase())) {
      errors.value.image = 'Accept file type: png, jpg, jpeg';
      return;
    } else if (file.size > 1048576) {
      errors.value.image = 'File size must be less than 1mb';
      return;
    }
    form.value.image_preview = URL.createObjectURL(file);
    errors.value.image = null;
    form.value.image = file;
  };
  input.click();
};

const removeImage = () => {
  form.value.image_remove = 1;
  form.value.image = null;
  form.value.image_preview = defaultImage;
};
</script>

Replace the contents of resources/js/component/operation/BalanceAdjustment.vue with the code below. If the file doesnโ€™t exist yet, please create it.

<template>
  <div class="vl-parent">
    <Loading v-model:active="isLoading" :is-full-page="true" color="blue" />

    <ShareModal ref="messageBox"></ShareModal>

    <!-- Form Modal -->
    <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
      data-bs-backdrop="static" data-bs-focus="false">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header py-2 bg-secondary text-light">
            <h5 class="modal-title" style="font-weight: bold">
              {{ form.id ? "Edit" : "Create" }} Balance Adjustment
            </h5>
          </div>
          <div class="modal-body">
            <form @submit.prevent="saveData" id="form">
              <div class="row">
                <div class="col-12 mb-3">
                  <label class="form-label required">Amount</label>
                  <div class="input-group" :class="{ 'is-invalid': errors.amount }">
                    <span class="input-group-text">$</span>
                    <input type="text" :class="['form-control', { 'is-invalid': errors.amount }]" v-model="form.amount"
                      ref="autofocus" />
                  </div>
                  <span v-if="errors.amount" class="invalid-feedback"> {{
                    errors.amount[0] }} </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Adjustment Type</label>
                  <select :class="['form-select', { 'is-invalid': errors.type_id }]" v-model="form.type_id">
                    <option v-for="(value, key) in typeList" :key="key" :value="key">
                      {{ value }}
                    </option>
                  </select>
                  <span v-if="errors.type_id" class="invalid-feedback"> {{ errors.type_id[0] }}
                  </span>
                </div>
                <div class="col-12 mb-3">
                  <label class="form-label required">Adjustment Date</label>
                  <flat-pickr v-model="form.adjustment_date" class="form-control" :config="dateConfig" />
                  <span v-if="errors.adjustment_date" class="invalid-feedback"> {{
                    errors.adjustment_date[0] }} </span>
                </div>

                <div class="col-12 mb-3">
                  <label class="form-label required">Remark</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.remark }]" v-model="form.remark" />
                  <span v-if="errors.remark" class="invalid-feedback"> {{
                    errors.remark[0] }} </span>
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
              <i class="bi bi-x-lg"></i> Cancel
            </button>
            <button type="submit" class="btn btn-primary px-3" form="form">
              <i class="bi bi-floppy" style="padding-right: 3px;"></i> Save
            </button>
          </div>
        </div>
      </div>
    </div>

    <button type="button" class="btn btn-primary" style="float: right" @click="openModal">
      <i class="bi bi-plus-circle"></i> Add New
    </button>

    <div class="pagetitle">
      <h1>Balance Adjustment</h1>
    </div>
    <section class="section">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <!-- Filter -->
            <form @submit.prevent="getData(true)">
              <div class="row pt-4">
                <div class="col-md-10">
                  <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Remark</label>
                      <input type="text" class="form-control" v-model="filter.remark" placeholder="Search..." />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Type</label>
                      <select class="form-select" v-model="filter.type_id">
                        <option value="0">ALL</option>
                        <option v-for="(value, key) in typeList" :key="key" :value="key">
                          {{ value }}
                        </option>
                      </select>
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">From Date</label>
                      <flat-pickr v-model="filter.from_date" class="form-control" :config="dateConfig"
                        @change="onStartChange" />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">To Date</label>
                      <flat-pickr v-model="filter.to_date" class="form-control" :config="dateConfig"
                        @change="onEndChange" />
                    </div>
                  </div>
                </div>
                <div class="col-md-2 align-self-end">
                  <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                  </button>
                </div>
              </div>
            </form>
            <hr class="text-secondary" />
            <!-- Data List -->
            <table class="table table-striped">
              <thead>
                <tr class="table-dark">
                  <th scope="col" width="50px">#</th>
                  <th scope="col" width="100px"> Action</th>
                  <th scope="col" @click="sortData('type_id')" style="cursor: pointer">
                    Type <i class="text-secondary"
                      :class="filter.sortBy == 'type_id' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('amount')" style="cursor: pointer">
                    Amount <i class="text-secondary"
                      :class="filter.sortBy == 'amount' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('remark')" style="cursor: pointer">
                    Remark <i class="text-secondary"
                      :class="filter.sortBy == 'remark' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('adjustment_date')" style="cursor: pointer">
                    Adjustment Date <i class="text-secondary"
                      :class="filter.sortBy == 'adjustment_date' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('created_at')" style="cursor: pointer" width="200px">
                    Created Time <i class="text-secondary"
                      :class="filter.sortBy == 'created_at' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr v-if="dataList && dataList.data && dataList.data.length > 0" v-for="(d, index) in dataList.data"
                  :key="d.id">
                  <th scope="row">{{ dataList.from + index }}</th>
                  <td>
                    <i class="bi bi-trash3-fill pe-3 text-danger" role="button" @click="deleteData(d.id)"></i>
                    <i class="bi bi-pencil-square text-success" role="button" @click="editData(d.id)"></i>
                  </td>
                  <td class="text-capitalize"
                    :class="{ 'text-danger': d.type_id == 2, 'text-success': d.type_id == 1 }">
                    {{ typeList[d.type_id] }}</td>
                  <td>{{ currencyFormat(d.amount) }}</td>
                  <td>{{ d.remark }}</td>
                  <td>{{ dateFormat(d.adjustment_date, true) }}</td>
                  <td>{{ dateFormat(d.created_at) }}</td>
                </tr>
                <tr v-else>
                  <td colspan="10" class="shadow-none">
                    No record found
                  </td>
                </tr>
              </tbody>
            </table>
            <!-- Pagination -->
            <div class="d-flex justify-content-end">
              <nav v-if="dataList.links && dataList.links.length > 3">
                <ul class="pagination">
                  <li :class="['page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                    v-for="data in dataList.links">
                    <span class="page-link" style="cursor: pointer" v-html="data.label" v-if="data.url && !data.active"
                      @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                    <span class="page-link" v-html="data.label" v-else></span>
                  </li>
                </ul>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { onMounted, onUnmounted, ref } from 'vue';
import { Modal } from 'bootstrap';
import { clearForm, currencyFormat, dateFormat, setFocus } from '../../helper.js';
import ShareModal from '../Share/Modal.vue';
import axios from 'axios';
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';

const isLoading = ref(false);
const formModalInstance = ref(null);
const formModal = ref(null);
const messageBox = ref(null);
const autofocus = ref(null);
const typeList = ref({ "1": "Credit (+)", "2": "Debit (-)" });

const form = ref(
  {
    id: null,
    type_id: null,
    adjustment_date: Date(),
    amount: null,
    remark: null
  }
);
const filter = ref(
  {
    remark: null,
    type_id: 0,
    from_date: null,
    to_date: null,
    sortBy: null,
    orderBy: null,
    page: 1
  }
);
const dataList = ref([]);
const errors = ref({});
onMounted(() => {
  if (formModal.value) {
    formModalInstance.value = new Modal(formModal.value);
    formModal.value.addEventListener("shown.bs.modal", () => {
      setFocus(autofocus);
    });
    formModal.value.addEventListener("hide.bs.modal", () => {
      document.activeElement?.blur();
    });
    formModal.value.addEventListener("hidden.bs.modal", () => {
      clearForm(form.value);
      errors.value = {};
    });
  }
  getData(true);
});
onUnmounted(() => {
  if (formModalInstance.value) {
    formModalInstance.value.dispose();
  }
});

// add or create
const openModal = () => {
  isLoading.value = false;
  formModalInstance.value.show();
};

// submit form
const saveData = () => {
  isLoading.value = true;
  axios[form.value.id > 0 ? "put" : "post"]("api/balance-adjustment/save", form.value)
    .then((response) => {
      if (response.data.success) {
        formModalInstance.value.hide();
        messageBox.value.showModal(1);
        getData();
      } else {
        errors.value = response.data.errors;
        setFocus(autofocus);
      }
    })
    .catch((ex) => {
      console.log(ex);
      setFocus(autofocus);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// load data
const getData = (resetPge = false) => {
  isLoading.value = true;
  if (resetPge)
    filter.value.page = 1;
  axios.post("api/balance-adjustment/list", filter.value).then((response) => {
    if (response.data.success) {
      dataList.value = response.data.data;
    }
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// Pagination
const paginate = (page_number) => {
  filter.value.page = page_number;
  if (page_number > dataList.last_page) {
    filter.value.page = dataList.last_page;
  }
  if (page_number <= 0) {
    filter.value.page = 1;
  }
  getData();
};

// sort
const sortData = (field) => {
  if (filter.value.sortBy === field) {
    filter.value.orderBy = filter.value.orderBy == 'asc' ? 'desc' : 'asc';
  } else {
    filter.value.sortBy = field;
    filter.value.orderBy = 'asc';
  }
  getData();
};

// edit
const editData = (id) => {
  isLoading.value = true;
  axios.get("api/balance-adjustment/edit/" + id).then((response) => {
    Object.keys(form.value).forEach(key => {
      if (key in response.data) {
        form.value[key] = response.data[key];
      }
    });
    formModalInstance.value.show();
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// delete
const deleteData = (id) => {
  messageBox.value.showModal(4, () => {
    isLoading.value = true;
    axios.delete("api/balance-adjustment/delete/" + id).then(() => {
      getData(true);
    })
      .catch((ex) => {
        console.log(ex);
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
};

const dateConfig = ref({
  wrap: true,
  altFormat: "d-M-Y",
  altInput: true,
  dateFormat: "Y-m-d",
  enableTime: false,
  defaultHour: "00",
  time_24hr: true,
});

const onStartChange = (selectedDates, dateStr, instance) => {
  dateConfig.value.minDate = dateStr;
};

const onEndChange = (selectedDates, dateStr, instance) => {
  dateConfig.value.maxDate = dateStr;
};

</script>

Replace the contents of resources/js/component/Setting/User.vue with the code below. If the file doesnโ€™t exist yet, please create it.

<template>
  <div class="vl-parent">
    <Loading v-model:active="isLoading" :is-full-page="true" color="blue" />

    <ShareModal ref="messageBox"></ShareModal>

    <!-- Form Modal -->
    <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
      data-bs-backdrop="static" data-bs-focus="false">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header py-2 bg-secondary text-light">
            <h5 class="modal-title" style="font-weight: bold">
              {{ form.id ? "Edit" : "Create" }} User
            </h5>
          </div>
          <div class="modal-body">
            <form @submit.prevent="saveData" id="form">
              <div class="row">
                <div class="col-12 mb-3">
                  <label class="form-label required">Username</label>
                  <input type="text" :class="['form-control', { 'is-invalid': errors.username }]"
                    :disabled="form.id > 0" v-model="form.username" ref="autofocus" />
                  <span v-if="errors.username" class="invalid-feedback"> {{ errors.username[0] }} </span>
                </div>
                <div class="mb-3">
                  <div class="row">
                    <div class="col-9">
                      <label for="role" class="form-label">Role</label>
                      <select id="role" name="role"
                        :class="['form-select text-capitalize', { 'is-invalid': errors.role }]" v-model="form.role">
                        <option v-for="role in ['admin', 'cashier', 'superadmin']">
                          {{ role }}
                        </option>
                      </select>
                      <span v-if="errors.role" class="invalid-feedback"> {{ errors.role[0] }} </span>
                    </div>
                    <div class="col-3">
                      <label for="active" class="form-label">Active</label>
                      <div class="form-check form-switch">
                        <input class="form-check-input" type="checkbox" role="switch" id="active" name="active"
                          style="cursor: pointer;" v-model="form.active" />
                      </div>
                    </div>
                  </div>
                </div>
                <div :class="['mb-3', { 'required': form.id == 0 }]">
                  <label for="password" class="form-label">Password</label>
                  <input id="password" name="password" type="password" v-model="form.password"
                    :class="['form-control', { 'is-invalid': errors.password }]" />
                  <span v-if="errors.password" class="invalid-feedback"> {{ errors.password[0] }} </span>
                </div>
                <div class="mb-3">
                  <label for="password_confirmation" class="form-label">Confirm Password</label>
                  <input id="password_confirmation" name="password_confirmation" type="password" class="form-control"
                    v-model="form.password_confirmation" />
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
              <i class="bi bi-x-lg"></i> Cancel
            </button>
            <button type="submit" class="btn btn-primary px-3" form="form">
              <i class="bi bi-floppy" style="padding-right: 3px;"></i> Save
            </button>
          </div>
        </div>
      </div>
    </div>

    <button type="button" class="btn btn-primary" style="float: right" @click="openModal">
      <i class="bi bi-plus-circle"></i> Add New
    </button>

    <div class="pagetitle">
      <h1>User</h1>
    </div>
    <section class="section">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <!-- Filter -->
            <form @submit.prevent="getData(true)">
              <div class="row pt-4">
                <div class="col-md-10">
                  <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Username</label>
                      <input type="text" class="form-control" v-model="filter.username" placeholder="Search..." />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                      <label class="form-label">Role</label>
                      <select class="form-select text-capitalize" v-model="filter.role">
                        <option value="0">ALL</option>
                        <option v-for="role in ['admin', 'cashier', 'superadmin']">
                          {{ role }}
                        </option>
                      </select>
                    </div>
                  </div>
                </div>
                <div class="col-md-2 align-self-end">
                  <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                  </button>
                </div>
              </div>
            </form>
            <hr class="text-secondary" />
            <!-- Data List -->
            <table class="table table-striped">
              <thead>
                <tr class="table-dark">
                  <th scope="col" width="50px">#</th>
                  <th scope="col" width="100px">
                    Action</th>
                  <th scope="col" @click="sortData('username')" style="cursor: pointer">
                    Username <i class="text-secondary"
                      :class="filter.sortBy == 'username' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('role')" style="cursor: pointer">
                    Role <i class="text-secondary"
                      :class="filter.sortBy == 'role' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th class="text-center" scope="col" @click="sortData('active')" style="cursor: pointer">
                    Active <i class="text-secondary"
                      :class="filter.sortBy == 'active' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                  <th scope="col" @click="sortData('created_at')" style="cursor: pointer" width="200px">
                    Created Time <i class="text-secondary"
                      :class="filter.sortBy == 'created_at' ? (filter.orderBy == 'desc' ? 'bi bi-sort-alpha-down-alt' : 'bi bi-sort-alpha-down') : 'bi bi-arrow-down-up'"></i>
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr v-if="dataList && dataList.data && dataList.data.length > 0" v-for="(d, index) in dataList.data"
                  :key="d.id">
                  <th scope="row">{{ dataList.from + index }}</th>
                  <td>
                    <i class="bi bi-trash3-fill pe-3 text-danger" role="button" @click="deleteData(d.id)"></i>
                    <i class="bi bi-pencil-square text-success" role="button" @click="editData(d.id)"></i>
                  </td>
                  <td>{{ d.username }}</td>
                  <td class="text-capitalize">{{ d.role }}</td>
                  <td class="text-center">
                    <i
                      :class="['bi fs-3', { 'bi-toggle2-on text-success': d.active, 'bi-toggle2-off text-danger': !d.active }]"></i>
                  </td>
                  <td>{{ dateFormat(d.created_at) }}</td>
                </tr>
                <tr v-else>
                  <td colspan="10" class="shadow-none">
                    No record found
                  </td>
                </tr>
              </tbody>
            </table>
            <!-- Pagination -->
            <div class="d-flex justify-content-end">
              <nav v-if="dataList.links && dataList.links.length > 3">
                <ul class="pagination">
                  <li :class="['page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                    v-for="data in dataList.links">
                    <span class="page-link" style="cursor: pointer" v-html="data.label" v-if="data.url && !data.active"
                      @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                    <span class="page-link" v-html="data.label" v-else></span>
                  </li>
                </ul>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { onMounted, onUnmounted, ref } from 'vue';
import { Modal } from 'bootstrap';
import { clearForm, dateFormat, setFocus } from '../../helper.js';
import ShareModal from '../Share/Modal.vue';
import axios from 'axios';

const isLoading = ref(false);
const formModalInstance = ref(null);
const formModal = ref(null);
const messageBox = ref(null);
const autofocus = ref(null);
const form = ref(
  {
    id: null,
    username: null,
    role: null,
    active: true,
    password: null,
    password_confirmation: null
  }
);
const filter = ref(
  {
    username: null,
    role: null,
    sortBy: null,
    orderBy: null,
    page: 1
  }
);
const dataList = ref([]);
const errors = ref({});
onMounted(() => {
  if (formModal.value) {
    formModalInstance.value = new Modal(formModal.value);
    formModal.value.addEventListener("shown.bs.modal", () => {
      setFocus(autofocus);
    });
    formModal.value.addEventListener("hide.bs.modal", () => {
      document.activeElement?.blur();
    });
    formModal.value.addEventListener("hidden.bs.modal", () => {
      clearForm(form.value);
      errors.value = {};
    });
  }
  getData(true);
});
onUnmounted(() => {
  if (formModalInstance.value) {
    formModalInstance.value.dispose();
  }
});

// add or create
const openModal = () => {
  isLoading.value = false;
  formModalInstance.value.show();
};

// submit form
const saveData = () => {
  isLoading.value = true;
  axios[form.value.id > 0 ? "put" : "post"]("api/user/save", form.value)
    .then((response) => {
      if (response.data.success) {
        formModalInstance.value.hide();
        messageBox.value.showModal(1);
        getData();
      } else {
        errors.value = response.data.errors;
        setFocus(autofocus);
      }
    })
    .catch((ex) => {
      console.log(ex);
      setFocus(autofocus);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// load data
const getData = (resetPge = false) => {
  isLoading.value = true;
  if (resetPge)
    filter.value.page = 1;
  axios.post("api/user/list", filter.value).then((response) => {
    if (response.data.success) {
      dataList.value = response.data.data;
    }
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// Pagination
const paginate = (page_number) => {
  filter.value.page = page_number;
  if (page_number > dataList.last_page) {
    filter.value.page = dataList.last_page;
  }
  if (page_number <= 0) {
    filter.value.page = 1;
  }
  getData();
};

// sort
const sortData = (field) => {
  if (filter.value.sortBy === field) {
    filter.value.orderBy = filter.value.orderBy == 'asc' ? 'desc' : 'asc';
  } else {
    filter.value.sortBy = field;
    filter.value.orderBy = 'asc';
  }
  getData();
};

// edit
const editData = (id) => {
  isLoading.value = true;
  axios.get("api/user/edit/" + id).then((response) => {
    Object.keys(form.value).forEach(key => {
      if (key in response.data) {
        form.value[key] = response.data[key];
      }
    });
    form.value.active = form.value.active ? true : false;
    formModalInstance.value.show();
  })
    .catch((ex) => {
      console.log(ex);
    })
    .finally(() => {
      isLoading.value = false;
    });
};

// delete
const deleteData = (id) => {
  messageBox.value.showModal(4, () => {
    isLoading.value = true;
    axios.delete("api/user/delete/" + id).then(() => {
      getData(true);
    })
      .catch((ex) => {
        console.log(ex);
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
};

</script>

Setup Vue Router

Configure Vue Router to manage navigation between different POS screens. This enables a seamless single-page experience in your Laravel Vue.js POS CRUD application.

Replace the contents of resources/js/router/index.js with the code below. If the file doesnโ€™t exist yet, please create it.

import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/store/auth';

const routes = [
    {
        name: "login",
        path: "/login",
        component: () => import("@/component/share/Login.vue"),
    },
    {
        path: "/",
        component: () => import("@/component/layout/Admin.vue"),
        meta: {
            requiresAuth: true
        },
        children: [
            // ========= Dashboard =========
            {
                path: "/",
                component: () => import("@/component/dashboard/Dashboard.vue"),
            },
            // ========= Data =========
            {
                path: "/table",
                component: () => import("@/component/data/Table.vue")
            },
            {
                path: "/product",
                component: () => import("@/component/data/Product.vue")
            },
            {
                path: "/product-category",
                component: () => import("@/component/data/ProductCategory.vue")
            },
            // ========= Operation =========
            {
                path: "/balance-adjustment",
                component: () => import("@/component/operation/BalanceAdjustment.vue")
            },
            // Setting           
            {
                path: "/user",
                component: () => import("@/component/setting/User.vue")
            },
            {
                path: "/:pathMatch(.*)*",
                component: () => import("@/component/share/404.vue"),
            }
        ],
    }
]

const router = createRouter({
    history: createWebHistory(),
    routes,
})
router.beforeEach(async (to, from, next) => {
    const auth = useAuthStore()
    if (auth.user === null) {
        await auth.getUser();
    }
    if (to.meta.requiresAuth && !auth.user) {
        next('/login')
    } else {
        next()
    }
})
export default router

Setup Route for Laravel API

Define API routes in Laravel to connect the backend logic with your Vue components. These routes form the backbone of your Laravel Vue.js POS CRUD operations.

Replace the contents of routes/api.php with the code below. If the file doesnโ€™t exist yet, please create it.

<?php
// Laravel Vue.js POS CRUD @ https://laravelcenter.com
use App\Http\Controllers\AuthController;
use App\Http\Controllers\BalanceAdjustmentController;
use App\Http\Controllers\ProductCategoryController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\TableController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout']);

Route::middleware('auth:sanctum')->group(function () {
    // Auth
    Route::prefix('auth')->controller(AuthController::class)->group(function () {
        Route::get('/user',  'user');
        Route::post('/change-password', [AuthController::class, 'changePassword']);
    });

    // Table
    Route::prefix('table')->controller(TableController::class)->group(function () {
        Route::post('list', 'list');
        Route::get('edit/{id}',  'edit');
        Route::match(['post', 'put'], 'save', 'save');
        Route::delete('delete/{id}', 'delete');
    });

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

    // Product
    Route::prefix('product')->controller(ProductController::class)->group(function () {
        Route::post('list', 'list');
        Route::get('edit/{id}',  'edit');
        Route::match(['post', 'put'], 'save', 'save');
        Route::delete('delete/{id}', 'delete');
        Route::get('category-list', 'categoryList');
    });

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

    // System User
    Route::prefix('user')->controller(UserController::class)->group(function () {
        Route::post('list', 'list');
        Route::get('edit/{id}',  'edit');
        Route::match(['post', 'put'], 'save', 'save');
        Route::delete('delete/{id}', 'delete');
    });
});

Additional Setup for Laravel Vue.js POS CRUD Functionality

Fixing Pagination Styling with Bootstrap in Laravel

By default, Laravel uses Tailwind CSS for pagination views. If your project is using Bootstrap (especially Bootstrap 4 or 5), the pagination links generated by {{ $items->links() }} may look unstyled or broken.

To fix this, you need to tell Laravel to use Bootstrap-compatible pagination views. You can do this by updating the AppServiceProvider.

Open the file:
app/Providers/AppServiceProvider.php

Inside the boot() method, add the following line:

use Illuminate\Pagination\Paginator;

public function boot()
{
    Paginator::useBootstrap();
}

This tells Laravel to render pagination links using Bootstrapโ€™s markup instead of Tailwind CSS.

When you’re handling image uploads in Laravel, the uploaded files are typically stored in the storage/app/public directory. However, these files need to be publicly accessible from the browser. Laravel provides a convenient command to create a symbolic link between the public/storage directory and storage/app/public:

php artisan storage:link

This command will generate a symbolic link so you can access uploaded images using a public URL like /storage/your-image.jpg.

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

Compile Assets

Use Vite to compile your Vue and CSS assets for efficient performance. This step ensures your Laravel Vue.js POS CRUD project runs smoothly in production.

Run the following command to compile your assets:

npm run dev

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

Once compiled, visit:

http://laravel-vuejs-pos

Setup Complete

Great work! ๐ŸŽ‰ Youโ€™ve now added seamless Vue-based navigation and implemented full CRUD functionality for managing key POS dataโ€”such as product categories, products, users, and more. Your Laravel Vue.js POS CRUD system is now fully interactive, modern, and easy to manage.

In the next tutorial, weโ€™ll move on to building essential POS features like the cart and order system. Youโ€™ll learn how to manage cart items, handle real-time updates, and process transactionsโ€”bringing your point-of-sale app closer to a real-world solution.

๐Ÿ‘‰ Continue to Part 5: POS Cart System

Laravel Vue.js POS Tutorial for Beginners Series

This step-by-step series will guide you through building a complete Laravel Vue.js POS system from scratch:

Leave a Reply

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