Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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.
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
]);
}
}
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>
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);
});
Bundle and compile your updated Vue components and ApexCharts integration using Vite. This ensures your Laravel Vue.js POS chart reports are fully functional in production.
Run the following command to compile your assets:
npm run dev
Use <strong>npm run build</strong>
for production.
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-visualizer
Here 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,
},
});
Once compiled, visit:
http://laravel-vuejs-pos
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! 🚀
This step-by-step series will guide you through building a complete Laravel Vue.js POS system from scratch: