Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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:
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.
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;
}
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();
}
}
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>
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
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');
});
});
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.
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
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
This step-by-step series will guide you through building a complete Laravel Vue.js POS system from scratch: