This Laravel Vue.js POS chart reports tutorial wraps up the series with beautiful interactive graphs using ApexCharts to present your data visually.
In this tutorial, you’ll learn how to visualize key business data using ApexCharts—a modern JavaScript charting library that integrates seamlessly with jQuery. We’ll create interactive charts that show total sales, daily trends, top products, and more.
By the end, your POS dashboard will not only be functional but visually insightful—giving store owners and admins a quick overview of performance without needing to open Excel files or read tables.
Table of Contents
Step 1: Setup Controller
Build a Laravel controller to process and return sales data tailored for chart rendering. This step is crucial for generating clean, structured data that powers your Laravel Vue.js POS chart reports.
Replace the contents of app/Http/Controllers/DashboardController.php with the code below. If the file doesn’t exist yet, please create it.
<?php
// Laravel Vue.js POS Chart Reports @https://laravelcenter.com
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Exception;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
public function __invoke()
{
try {
// Top Products
$top_products = Order::join('order_details', 'order_details.order_id', '=', 'orders.id')
->selectRaw('order_details.description,sum(order_details.qty) AS qty')
->where(DB::raw('DATE_FORMAT(orders.created_at,"%Y-%m-%d")'), DB::raw('DATE_FORMAT(CURDATE(),"%Y-%m-%d")'))
->groupBy('order_details.description')
->orderBy('qty', 'desc')
->take(10)->get();
// Sale by Categories
$sale_categories = Order::join('order_details', 'orders.id', '=', 'order_details.order_id')
->join('product_categories', 'product_categories.id', '=', 'order_details.product_category_id')
->select(
DB::raw("product_categories.name,sum((order_details.qty * order_details.unit_price*order_details.discount/100) + (order_details.qty * order_details.unit_price * (1-order_details.discount/100) * orders.discount/100)) as discount, sum(order_details.qty * order_details.unit_price) as total")
)
->where(DB::raw('DATE_FORMAT(orders.created_at,"%Y-%m-%d")'), DB::raw('DATE_FORMAT(CURDATE(),"%Y-%m-%d")'))
->groupBy(DB::raw('product_categories.name'))
->orderBy('product_categories.name')
->get();
// Last 15 days total sale amount
$days = 15;
$data = Order::select(DB::raw("DATE_FORMAT(created_at,'%Y-%m-%d') AS dd, sum(net_amount) AS total"))
->where(DB::raw('DATE_FORMAT(created_at,"%Y-%m-%d")'), '>=', DB::raw('DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL ' . $days . ' DAY),"%Y-%m-%d")'))
->groupBy('dd')
->orderBy('dd')
->get()->toArray();
$result = [];
for ($i = $days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime('-' . $i . ' days'));
$total = 0;
foreach ($data as $row) {
if ($row['dd'] == $date) {
$total = $row['total'];
break;
}
}
array_push($result, [
'date' => date('d-M', strtotime($date)),
'total' => $total
]);
}
} catch (Exception $ex) {
abort($ex->getCode(), $ex->getMessage());
}
return response()->json([
'success' => true,
'bar_data' => $result,
'sale_categories' => $sale_categories,
'top_products' => $top_products
]);
}
}
Step 2: Setup Vue Component
Create a Vue component that integrates with ApexCharts to display real-time sales charts. This component transforms your Laravel Vue.js POS chart reports into interactive, visual insights.
Replace the contents of resources/js/component/dashboard/Dashboard.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" />
<div class="pagetitle">
<h1>Dashboard</h1>
</div>
<div class="row">
<div class="col-md-4">
<div class="card shadow bg-primary text-white">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="me-2">
<div class="display-6 text-white">
{{ currencyFormat(summaryData.grand_total) }}
</div>
<div class="card-text fs-6 mt-2">Daily Total Sale</div>
</div>
<div style="color: lightblue"><i class="bi bi-cash-stack display-4"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow bg-danger text-white">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="me-2">
<div class="display-6 text-white">{{ currencyFormat(summaryData.total_discount) }}</div>
<div class="card-text fs-6 mt-2">Daily Total Disount</div>
</div>
<div style="color: lightgray;"><i class="bi bi-percent display-4"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow bg-success text-white">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="me-2">
<div class="display-6 text-white">{{ currencyFormat(summaryData.net_amount) }}</div>
<div class="card-text fs-6 mt-2">Daily Net Amount</div>
</div>
<div style="color: lightblue"><i class="bi bi-coin display-4"></i></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card shadow">
<strong class="card-header bg-dark text-white py-2">
<i class="fas fa-chart-area me-1"></i>
Daily Sale Report By Categories
</strong>
<div class="card-body">
<VueApexCharts height="390px" :options="ProductCategoryOptions" :series="ProductCategorySeries">
</VueApexCharts>
</div>
</div>
</div>
<div class="col-md-6">
<table class="table table-hover shadow align-middle bg-white">
<thead class="table-dark">
<tr>
<th style="width: 50px">#</th>
<th>Product Name</th>
<th class="text-end">QTY</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, index) in Top10Products">
<td>{{ index + 1 }}</td>
<td>{{ data.description }}</td>
<td class="text-end">
{{ data.qty }}
</td>
</tr>
<tr v-for="item in (10 - Top10Products.length)" :key="item">
<td>{{ (Top10Products.length) + item }}</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col">
<div class="card shadow mb-3">
<strong class="card-header bg-dark text-white py-2">
<i class="fas fa-chart-bar me-1"></i>
15 Days Total Sale Amount
</strong>
<div class="card-body">
<VueApexCharts height="400px" type="bar" :options="chartOptions" :series="series">
</VueApexCharts>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
import { currencyFormat } from "../../helper";
import { onMounted, ref } from "vue";
const VueApexCharts = defineAsyncComponent(() =>
import('vue3-apexcharts')
);
const isLoading = ref(false);
const summaryData = ref({
grand_total: 0,
total_discount: 0,
net_amount: 0
});
const Top10Products = ref([]);
const ProductCategorySeries = ref([]);
const ProductCategoryOptions = ref({
colors: ['#3366cc', '#660066', '#006600', '#cc0066', '#996633', '#006666', '#993399', '#999966', '#ffcc99', '#33cc33', '#cccc00'],
fill: {
colors: ['#3366cc', '#660066', '#006600', '#cc0066', '#996633', '#006666', '#993399', '#999966', '#ffcc99', '#33cc33', '#cccc00']
},
chart: {
type: 'pie',
},
labels: [],
responsive: [{
options: {
legend: {
position: 'bottom'
}
}
}],
legend: {
position: 'bottom'
},
theme: {
palette: 'palette2' // upto palette10
},
dataLabels: {
enabled: true,
formatter: function (val) {
return val.toFixed(2) + "%"
},
dropShadow: {
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (value) {
try {
if (typeof value !== "number") {
if (value && !isNaN(value)) {
value = parseFloat(value);
} else {
return value;
}
}
var formatter = new Intl.NumberFormat("en-US", {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return "$" + formatter.format(value);
} catch (ex) {
return "$" + value;
}
}
}
},
});
const series = ref([
{
name: 'Net Amount',
data: []
},
]);
const chartOptions = ref({
chart: {
//height: 350,
type: 'bar',
stacked: true,
toolbar: {
show: false,
},
},
stroke: {
curve: 'smooth',
},
colors: ['#198754', '#0d6efd', '#9C27B0'],
fill: {
opacity: 1,
gradient: {
inverseColors: false,
shade: 'light',
type: "vertical",
opacityFrom: 0.85,
opacityTo: 0.55,
},
colors: ['#198754', '#0d6efd', '#9C27B0'],
},
labels: [],
markers: {
size: 0
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (value) {
try {
if (typeof value !== "number") {
if (value && !isNaN(value)) {
value = parseFloat(value);
} else {
return value;
}
}
var formatter = new Intl.NumberFormat("en-US", {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return "$" + formatter.format(value);
} catch (ex) {
return "$" + value;
}
}
}
},
dataLabels: {
enabled: true,
formatter: function (value) {
try {
if (typeof value !== "number") {
if (value && !isNaN(value)) {
value = parseFloat(value);
} else {
return value;
}
}
var formatter = new Intl.NumberFormat("en-US", {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return "$" + formatter.format(value);
} catch (ex) {
return "$" + value;
}
}
}
});
onMounted(() => {
isLoading.value = true;
axios.get("api/dashboard")
.then((response) => {
if (response.data.success) {
// Daily Summary Data
if (response.data.sale_categories && response.data.sale_categories.length > 0) {
var d = response.data.sale_categories;
for (var i = 0; i < d.length; i++) {
ProductCategoryOptions.value.labels.push(d[i].name);
ProductCategorySeries.value.push(Number(d[i].total) - Number(d[i].discount));
summaryData.value.grand_total += Number(d[i].total);
summaryData.value.total_discount += Number(d[i].discount);
};
summaryData.value.net_amount = (summaryData.value.grand_total - summaryData.value.total_discount) ?? 0;
}
// Top 10 Products
Top10Products.value = response.data.top_products;
// Last 15 days
if (response.data.bar_data && response.data.bar_data.length > 0) {
response.data.bar_data.forEach(element => {
chartOptions.value.labels.push(element.date);
series.value[0].data.push(Number(element.total));
});
}
}
})
.catch((ex) => {
console.log(ex);
})
.finally(() => {
isLoading.value = false;
});
});
</script>Step 3: Define API Route
Define Laravel API routes to deliver chart data to the frontend securely and efficiently. These endpoints serve as the data backbone of your Laravel Vue.js POS chart reports.
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 Chart Reports @https://laravelcenter.com
use App\Http\Controllers\AuthController;
use App\Http\Controllers\BalanceAdjustmentController;
use App\Http\Controllers\CashierController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProductCategoryController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\ReportController;
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');
});
// Cashier
Route::prefix('cashier')->controller(CashierController::class)->group(function () {
Route::get('/', 'index');
Route::get('show-table/{id?}', 'showTable');
Route::post('select-table', 'selectTable');
Route::post('update-order-qty', 'updateOrderQty');
Route::post('update-detail-discount', 'updateDetailDiscount');
Route::post('update-order-discount', 'updateOrderDiscount');
Route::delete('delete-order/{product_id}/{table_id}', 'deleteOrder');
Route::post('add-to-order', 'addToOrder');
Route::post('print-invoice', 'printInvoice');
Route::post('confirm-payment', 'confirmPayment');
});
// Report
Route::prefix('report')->controller(ReportController::class)->group(function () {
Route::post('sale-summary', 'saleSummary');
Route::post('product-summary', 'productSummary');
Route::post('sale-history', 'saleHistory');
Route::post('sale-history-summary', 'saleHistorySummary');
Route::post('export-product-summary', 'exportProductSummary');
Route::post('export-sale-history', 'exportSaleHistory');
Route::get('show-order-detail/{id}', 'showOrderDetail');
});
// Dashboard
Route::get('dashboard', DashboardController::class);
});Running and Testing Your Project
To better understand and optimize your Vite build, you can integrate the powerful rollup-plugin-visualizer. This plugin generates a visual report of your bundle’s contents, helping you identify large dependencies, duplicated modules, and potential areas for optimization.
By adding it to your Vite config, you’ll be able to analyze your final production build as an interactive treemap or sunburst chart—making it easier to improve loading performance and keep your Laravel Vue.js POS project lightweight and fast.
This tool is especially useful during production builds (vite build) when performance matters most.
Run the following command to install rollup-plugin-visualizer.
npm install rollup-plugin-visualizerHere is the final version of vite.config.js for this project.
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.js'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
})
],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.split('node_modules/')[1].split('/')[0];
}
},
},
plugins: [
visualizer({
open: true, // Automatically opens the report in the browser
filename: 'bundle-report.html', // Output file
gzipSize: true, // Show Gzip sizes
brotliSize: true, // Show Brotli sizes
}),
],
},
chunkSizeWarningLimit: 1000,
},
});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 – Laravel Vue.js POS Chart Reports
Fantastic job! 🎉 You’ve successfully enhanced your Laravel Vue.js POS system by adding powerful graph reports using ApexCharts. Now your users can visualize sales trends, top products, and key metrics through beautiful, interactive charts—all right inside the dashboard.
With this final feature, your POS app is not only functional but also insightful and professional, ready to help business owners make data-driven decisions with ease.
Thank you for following along this complete Laravel Vue.js POS tutorial series! 🚀
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.







