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.
Table of Contents
Step 1: 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;
}Step 2: 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();
}
}Step 3: 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>Step 4: 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 routerStep 5: 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.
Set Up Laravel Storage Symlink
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:linkThis 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.
Running and Testing Your Project
Open your Terminal/CMD in separate windows, go to the project’s root folder, and then run the command below:
npm run devphp artisan serveWith both commands running in their separate windows, open your web browser to the Laravel address (http://127.0.0.1:8000).





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:
- Part 1: Install Laravel Framework
Set up a fresh Laravel 12 project and configure the environment and database for your POS backend. - Part 2: Create Vue SPA & Integrate NiceAdmin
Convert Laravel into a Vue.js SPA with Vue Router, install Vue 3, Bootstrap 5, and integrate the NiceAdmin template for a modern UI. - Part 3: Data Migration & Authentication
Migrate tables for users, roles, and products. Add authentication using Laravel Sanctum. - Part 4: Navigation & CRUD Operations
Build dynamic sidebar navigation and implement CRUD for products, categories, and users using API and Vue components. - Part 5: POS Cart System
Create a POS cart where users can add, update, and remove items using Vue 3’s built-in reactivity. - Part 6: Reports & Sales Filters
Add sales report filtering by date, user, or payment. Enable Excel export and receipt printing. - Part 7: Dashboard with ApexCharts
Display real-time sales data in responsive charts using ApexCharts.







