feat(analytic): membuat halaman analitik

This commit is contained in:
Yoga Pangestu 2024-11-28 19:46:25 +07:00
parent 5f8b09f9d1
commit 423856d740
11 changed files with 709 additions and 0 deletions

View File

@ -0,0 +1,190 @@
<?php
namespace App\Http\Controllers\Admin\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Expense;
use App\Models\Inventory;
use App\Models\Service;
use App\Models\Transaction;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AnalyticController extends Controller
{
public function index(): View
{
$totalIncome = Transaction::query()->barbershop()->sum('total');
$totalExpense = Expense::query()->barbershop()->sum('amount');
$totalShaving = Transaction::query()->barbershop()->count();
$topService = Service::query()->barbershop()->withCount('transactions')->get();
$paymentMethod = Transaction::select('payment_method', DB::raw('SUM(total) as total_amount'))
->barbershop()
->groupBy('payment_method')
->get();
$incomeByBarber = User::with('biography')->barbershop()->barber()->withSum('transactions', 'total')->get();
$transactionByBarber = User::with('biography')->barbershop()->barber()->withCount('transactions')->get();
$totalAsset = Inventory::query()->barbershop()->sum(DB::raw('quantity * price'));
$topBarber = User::with('biography')
->barbershop()
->barber()
->withSum('transactions', 'total')
->orderBy('transactions_sum_total', 'desc')
->first();
$totalUser = User::query()->barbershop()->count();
$totalBarber = User::query()->barbershop()->barber()->count();
return view('pages.admin.dashboard.analytic', [
'pageTitle' => 'Analitik',
'totalIncome' => $totalIncome,
'totalExpense' => $totalExpense,
'totalShaving' => $totalShaving,
'topService' => $topService,
'paymentMethod' => $paymentMethod,
'incomeByBarber' => $incomeByBarber,
'transactionByBarber' => $transactionByBarber,
'totalAsset' => $totalAsset,
'topBarber' => $topBarber,
'totalUser' => $totalUser,
'totalBarber' => $totalBarber,
]);
}
public function filter(Request $request): JsonResponse
{
$startDate = $request->start_date;
$endDate = $request->end_date;
$totalIncome = Transaction::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->sum('total');
$totalExpense = Expense::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->sum('amount');
$totalShaving = Transaction::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->count();
$topService = Service::query()
->barbershop()
->withCount(['transactions' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
]);
}])
->get();
$paymentMethod = Transaction::select('payment_method', DB::raw('SUM(total) as total_amount'))
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->groupBy('payment_method')
->get();
$incomeByBarber = User::with('biography')
->barbershop()
->barber()
->withSum(['transactions' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
]);
}], 'total')
->get();
$transactionByBarber = User::with('biography')
->barbershop()
->barber()
->withCount(['transactions' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
]);
}])
->get();
$totalAsset = Inventory::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->sum(DB::raw('quantity * price'));
$topBarber = User::with('biography')
->barbershop()
->barber()
->withSum(['transactions' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
]);
}], 'total')
->orderBy('transactions_sum_total', 'desc')
->first();
$totalUser = User::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->count();
$totalBarber = User::query()
->barbershop()
->whereBetween('created_at', [
Carbon::parse($startDate)->startOfDay(),
Carbon::parse($endDate)->endOfDay(),
])
->barber()
->count();
return response()->json([
'status' => true,
'message' => 'Data Successfully Retrieved',
'data' => [
'startDate' => $startDate,
'endDate' => $endDate,
'totalIncome' => $totalIncome,
'totalExpense' => $totalExpense,
'totalShaving' => $totalShaving,
'topService' => $topService,
'paymentMethod' => $paymentMethod,
'incomeByBarber' => $incomeByBarber,
'transactionByBarber' => $transactionByBarber,
'totalAsset' => $totalAsset,
'topBarber' => $topBarber,
'totalUser' => $totalUser,
'totalBarber' => $totalBarber,
],
], 200);
}
}

View File

@ -0,0 +1,42 @@
let incomeExpenseCtx = document.getElementById('incomeExpenseChart').getContext('2d');
let incomeExpenseChart = new Chart(incomeExpenseCtx, {
type: 'pie',
data: {
labels: ['Pendapatan', 'Pengeluaran'],
datasets: [{
label: 'Pendapatan vs Pengeluaran',
data: [totalIncome, totalExpense],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)'
],
borderColor: [
'rgb(75, 192, 192)',
'rgb(255, 99, 132)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
tooltip: {
callbacks: {
label: function (tooltipItem) {
let rupiahFormat = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
useGrouping: true
}).format(tooltipItem.raw);
return tooltipItem.label + ': ' + rupiahFormat.replace('Rp ', 'Rp').replace(/\s/g, '');
}
}
}
}
}
});

View File

@ -0,0 +1,44 @@
let incomeByBarberCtx = document.getElementById('incomeByBarberChart').getContext('2d');
let incomeByBarberLabels = incomeByBarber.map(incomeByBarber => incomeByBarber.biography.full_name);
let incomeByBarberData = incomeByBarber.map(incomeByBarber => incomeByBarber.transactions_sum_total);
let incomeByBarberBackgroundColors = incomeByBarberData.map(() => getRandomColor());
let incomeByBarberBorderColors = incomeByBarberBackgroundColors.map(color => color.replace('0.2', '1'));
let incomeByBarberChart = new Chart(incomeByBarberCtx, {
type: 'pie',
data: {
labels: incomeByBarberLabels,
datasets: [{
label: 'Pendapatan',
data: incomeByBarberData,
backgroundColor: incomeByBarberBackgroundColors,
borderColor: incomeByBarberBorderColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
tooltip: {
callbacks: {
label: function (tooltipItem) {
let rupiahFormat = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
useGrouping: true
}).format(tooltipItem.raw);
return tooltipItem.label + ': ' + rupiahFormat.replace('Rp ', 'Rp').replace(/\s/g, '');
}
}
}
}
}
});

View File

@ -0,0 +1,56 @@
let paymentMethodCtx = document.getElementById('paymentMethodChart').getContext('2d');
let paymentMethodLabels = paymentMethod.map(payment => {
switch (payment.payment_method) {
case '1':
return 'Tunai';
case '2':
return 'Qris';
case '3':
return 'Lainnya';
default:
return 'Unknown';
}
});
let paymentMethodData = paymentMethod.map(paymentMethod => paymentMethod.total_amount);
let paymentMethodBackgroundColors = paymentMethodData.map(() => getRandomColor());
let paymentMethodBorderColors = paymentMethodBackgroundColors.map(color => color.replace('0.2', '1'));
let paymentMethodChart = new Chart(paymentMethodCtx, {
type: 'doughnut',
data: {
labels: paymentMethodLabels,
datasets: [{
label: 'Jumlah Transaksi',
data: paymentMethodData,
backgroundColor: paymentMethodBackgroundColors,
borderColor: paymentMethodBorderColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
tooltip: {
callbacks: {
label: function (tooltipItem) {
let rupiahFormat = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
useGrouping: true
}).format(tooltipItem.raw);
return tooltipItem.label + ': ' + rupiahFormat.replace('Rp ', 'Rp').replace(/\s/g, '');
}
}
}
}
}
});

View File

@ -0,0 +1,30 @@
let topServiceCtx = document.getElementById('topServiceChart').getContext('2d');
let topServiceLabels = topService.map(topService => topService.name);
let topServiceData = topService.map(topService => topService.transactions_count);
let topServiceBackgroundColors = topServiceData.map(() => getRandomColor());
let topServiceBorderColors = topServiceBackgroundColors.map(color => color.replace('0.2', '1'));
let topServiceChart = new Chart(topServiceCtx, {
type: 'polarArea',
data: {
labels: topServiceLabels,
datasets: [{
label: 'Jumlah Transaksi',
data: topServiceData,
backgroundColor: topServiceBackgroundColors,
borderColor: topServiceBorderColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
}
},
});

View File

@ -0,0 +1,30 @@
let transactionByBarberCtx = document.getElementById('transactionByBarberChart').getContext('2d');
let transactionByBarberLabels = transactionByBarber.map(transactionByBarber => transactionByBarber.biography.full_name);
let transactionByBarberData = transactionByBarber.map(transactionByBarber => transactionByBarber.transactions_count);
let transactionByBarberBackgroundColors = transactionByBarberData.map(() => getRandomColor());
let transactionByBarberBorderColors = transactionByBarberBackgroundColors.map(color => color.replace('0.2', '1'));
let transactionByBarberChart = new Chart(transactionByBarberCtx, {
type: 'bar',
data: {
labels: transactionByBarberLabels,
datasets: [{
label: 'Jumlah Transaksi',
data: transactionByBarberData,
backgroundColor: transactionByBarberBackgroundColors,
borderColor: transactionByBarberBorderColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
}
}
});

View File

@ -0,0 +1,92 @@
$(document).ready(function () {
function formatToRupiah(amount) {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
useGrouping: true
}).format(amount).replace('Rp ', 'Rp').replace(/\s/g, '');
}
$("#filter-button").on("click", function (e) {
e.preventDefault();
const startDate = $("#start_date").val();
const endDate = $("#end_date").val();
$.ajax({
url: "/dashboard/analytic/filter",
method: "GET",
data: {
start_date: startDate,
end_date: endDate,
_token: $('meta[name="csrf-token"]').attr('content')
},
success: function (response) {
$("#total-asset").text(formatToRupiah(response.data.totalAsset));
$("#total-barber").text(response.data.totalBarber);
$("#total-user").text(response.data.totalUser);
$("#top-barber-name").text(response.data.topBarber.biography.full_name || "-deleted-");
$("#top-barber-total").text(formatToRupiah(response.data.topBarber.transactions_sum_total));
// income expense
let totalIncome = response.data.totalIncome;
let totalExpense = response.data.totalExpense;
incomeExpenseChart.data.datasets[0].data = [
totalIncome,
totalExpense,
];
incomeExpenseChart.update();
// top service
let topService = response.data.topService;
let topServiceLabels = topService.map(item => item.name);
let topServiceData = topService.map(item => item.transactions_count);
topServiceChart.data.labels = topServiceLabels;
topServiceChart.data.datasets[0].data = topServiceData;
topServiceChart.update();
// payment method
let paymentMethod = response.data.paymentMethod;
let paymentMethodLabels = paymentMethod.map(item => {
switch (item.payment_method) {
case '1':
return 'Tunai';
case '2':
return 'Qris';
case '3':
return 'Lainnya';
default:
return 'Unknown';
}
});
let paymentMethodData = paymentMethod.map(item => item.total_amount);
paymentMethodChart.data.labels = paymentMethodLabels;
paymentMethodChart.data.datasets[0].data = paymentMethodData;
paymentMethodChart.update();
// income by barber
let updatedIncomeByBarber = response.data.incomeByBarber;
let updatedLabels = updatedIncomeByBarber.map(item => item.biography.full_name);
let updatedData = updatedIncomeByBarber.map(item => item.transactions_sum_total);
incomeByBarberChart.data.labels = updatedLabels;
incomeByBarberChart.data.datasets[0].data = updatedData;
incomeByBarberChart.update();
// transaction by barber
let transactionByBarber = response.data.transactionByBarber;
let transactionByBarberLabels = transactionByBarber.map(item => item.biography.full_name);
let transactionByBarberData = transactionByBarber.map(item => item.transactions_count);
transactionByBarberChart.data.labels = transactionByBarberLabels;
transactionByBarberChart.data.datasets[0].data = transactionByBarberData;
transactionByBarberChart.update();
},
error: function (xhr) { },
});
});
});

View File

@ -0,0 +1,6 @@
function getRandomColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgba(${r}, ${g}, ${b}, 0.2)`;
}

View File

@ -0,0 +1,209 @@
@extends('layouts.admin')
@section('styles')
<link rel="stylesheet" type="text/css" href="{{ asset('assets/admin/css/vendors/flatpickr/flatpickr.min.css') }}">
@endsection
@section('app')
<div class="container-fluid">
<div class="mb-3">
<div class="input-group">
<div class="flatpicker-calender">
<input name="start_date" class="form-control human-friendly" id="start_date" type="date"
value="{{ $startDate ?? '2024-01-01' }}">
</div>
<span class="input-group-text">To</span>
<div class="flatpicker-calender">
<input name="end_date" class="form-control human-friendly" id="end_date" type="date"
value="{{ $endDate ?? now() }}">
</div>
<button class="btn btn-primary" id="filter-button">Filter</button>
</div>
</div>
<div class="row widget-grid">
<div class="col-12 col-md-6 col-lg-4">
<div class="card small-widget">
<div class="card-body primary">
<span class="f-light">Total Aset</span>
<div class="d-flex align-items-end gap-1">
<h4 id="total-asset">{{ formatToRupiah($totalAsset) }}</h4>
</div>
<div class="bg-gradient">
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<ul class="user-list">
<li>
<div class="user-icon primary">
<div class="user-box">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="-3 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-user-plus font-primary">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
</svg>
</div>
</div>
<div>
<h5 class="mb-1" id="total-barber">{{ $totalBarber }}</h5>
<span class="font-primary d-flex align-items-center">
<i class="icon-arrow-up icon-rotate me-1"> </i>
<span class="f-w-500">Barber</span>
</span>
</div>
</li>
<li>
<div class="user-icon success">
<div class="user-box">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="-3 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-user-minus font-success">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
</svg>
</div>
</div>
<div>
<h5 class="mb-1" id="total-user">{{ $totalUser }}</h5>
<span class="font-danger d-flex align-items-center">
<i class="icon-arrow-down icon-rotate me-1"></i>
<span class="f-w-500">Semua</span>
</span>
</div>
</li>
</ul>
</div>
</div>
<div class="card 0-hidden">
<div class="card-body balance-widget">
<span class="f-w-500 f-light">Top Barber</span>
<h4 class="mb-3 mt-1 f-w-500 mb-0 f-22">
<span class="counter"
id="top-barber-name">{{ $topBarber->biography->full_name ?? '-deleted-' }}</span>
</h4>
<span class=" btn-hover-effect f-w-500"
id="top-barber-total">{{ formatToRupiah($topBarber->transactions_sum_total) }}</span>
<div class="mobile-right-img">
<img class="mobile-img" src="{{ asset('assets/admin/images/crown.png') }}"
alt="mobile with coin">
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-header card-no-border">
<h5>Grafik Keuangan</h5>
</div>
<div class="card-body pt-0">
<div class="row m-0 overall-card">
<div class="chart-right">
<div class="card-body p-0">
<div class="current-sale-container">
<canvas id="incomeExpenseChart" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-header card-no-border">
<h5>Top Layanan</h5>
</div>
<div class="card-body pt-0">
<div class="row m-0 overall-card">
<div class="chart-right">
<div class="card-body p-0">
<div class="current-sale-container">
<canvas id="topServiceChart" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-header card-no-border">
<h5>Metode Pembayaran</h5>
</div>
<div class="card-body pt-0">
<div class="row m-0 overall-card">
<div class="chart-right">
<div class="card-body p-0">
<div class="current-sale-container">
<canvas id="paymentMethodChart" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-header card-no-border">
<h5>Pendapatan By Barber</h5>
</div>
<div class="card-body pt-0">
<div class="row m-0 overall-card">
<div class="chart-right">
<div class="card-body p-0">
<div class="current-sale-container">
<canvas id="incomeByBarberChart" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-header card-no-border">
<h5>Transaksi By Barber</h5>
</div>
<div class="card-body pt-0">
<div class="row m-0 overall-card">
<div class="chart-right">
<div class="card-body p-0">
<div class="current-sale-container">
<canvas id="transactionByBarberChart" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('beforeScripts')
<script src="{{ asset('assets/admin/js/flat-pickr/flatpickr.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/flat-pickr/custom-flatpickr.js?ver=1.0.0') }}"></script>
@endsection
@section('afterScripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
<script>
const totalIncome = @json($totalIncome);
const totalExpense = @json($totalExpense);
const topService = @json($topService);
const paymentMethod = @json($paymentMethod);
const incomeByBarber = @json($incomeByBarber);
const transactionByBarber = @json($transactionByBarber);
</script>
<script src="{{ asset('assets/admin/js/random-color.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/chart/finance.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/chart/payment-method.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/chart/top-service.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/chart/income-by-barber.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/chart/transaction-by-barber.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/filter.js?ver=1.0.0') }}"></script>
@endsection

View File

@ -49,6 +49,13 @@
<span>Ringkasan</span>
</a>
</li>
<li class="sidebar-list">
<i class="fa fa-thumb-tack"></i>
<a class="sidebar-link sidebar-title link-nav" href="{{ route('dashboard.analytic') }}">
<i data-feather="bar-chart-2"></i>
<span>Analitik</span>
</a>
</li>
<li class="sidebar-main-title">
<div>
<h6>Master</h6>

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Admin\Auth\LoginController;
use App\Http\Controllers\Admin\Dashboard\AnalyticController;
use App\Http\Controllers\Admin\Dashboard\OverviewController;
use App\Http\Controllers\Admin\Finance\CashController;
use App\Http\Controllers\Admin\Finance\ExpenseController;
@ -29,6 +30,8 @@
'prefix' => 'dashboard',
], function () {
Route::get('overview', [OverviewController::class, 'index'])->name('dashboard.overview');
Route::get('analytic', [AnalyticController::class, 'index'])->name('dashboard.analytic');
Route::get('analytic/filter', [AnalyticController::class, 'filter'])->name('dashboard.analytic.filter');
Route::prefix('master')->group(function () {
Route::prefix('barbershop')->group(function () {