From 423856d740677f67acaaf67045663532c13b77ad Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 28 Nov 2024 19:46:25 +0700 Subject: [PATCH] feat(analytic): membuat halaman analitik --- .../Admin/Dashboard/AnalyticController.php | 190 ++++++++++++++++ public/assets/admin/js/chart/finance.js | 42 ++++ .../assets/admin/js/chart/income-by-barber.js | 44 ++++ .../assets/admin/js/chart/payment-method.js | 56 +++++ public/assets/admin/js/chart/top-service.js | 30 +++ .../admin/js/chart/transaction-by-barber.js | 30 +++ public/assets/admin/js/filter.js | 92 ++++++++ public/assets/admin/js/random-color.js | 6 + .../pages/admin/dashboard/analytic.blade.php | 209 ++++++++++++++++++ .../views/partials/admin/_sidebar.blade.php | 7 + routes/web.php | 3 + 11 files changed, 709 insertions(+) create mode 100644 app/Http/Controllers/Admin/Dashboard/AnalyticController.php create mode 100644 public/assets/admin/js/chart/finance.js create mode 100644 public/assets/admin/js/chart/income-by-barber.js create mode 100644 public/assets/admin/js/chart/payment-method.js create mode 100644 public/assets/admin/js/chart/top-service.js create mode 100644 public/assets/admin/js/chart/transaction-by-barber.js create mode 100644 public/assets/admin/js/filter.js create mode 100644 public/assets/admin/js/random-color.js create mode 100644 resources/views/pages/admin/dashboard/analytic.blade.php diff --git a/app/Http/Controllers/Admin/Dashboard/AnalyticController.php b/app/Http/Controllers/Admin/Dashboard/AnalyticController.php new file mode 100644 index 0000000..9c2c74b --- /dev/null +++ b/app/Http/Controllers/Admin/Dashboard/AnalyticController.php @@ -0,0 +1,190 @@ +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); + } +} diff --git a/public/assets/admin/js/chart/finance.js b/public/assets/admin/js/chart/finance.js new file mode 100644 index 0000000..9b88b54 --- /dev/null +++ b/public/assets/admin/js/chart/finance.js @@ -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, ''); + } + } + } + } + } +}); diff --git a/public/assets/admin/js/chart/income-by-barber.js b/public/assets/admin/js/chart/income-by-barber.js new file mode 100644 index 0000000..24ddb0c --- /dev/null +++ b/public/assets/admin/js/chart/income-by-barber.js @@ -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, ''); + } + } + } + } + } +}); diff --git a/public/assets/admin/js/chart/payment-method.js b/public/assets/admin/js/chart/payment-method.js new file mode 100644 index 0000000..b28bec2 --- /dev/null +++ b/public/assets/admin/js/chart/payment-method.js @@ -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, ''); + } + } + } + } + } +}); diff --git a/public/assets/admin/js/chart/top-service.js b/public/assets/admin/js/chart/top-service.js new file mode 100644 index 0000000..58e9a20 --- /dev/null +++ b/public/assets/admin/js/chart/top-service.js @@ -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', + }, + } + }, +}); diff --git a/public/assets/admin/js/chart/transaction-by-barber.js b/public/assets/admin/js/chart/transaction-by-barber.js new file mode 100644 index 0000000..8c7312b --- /dev/null +++ b/public/assets/admin/js/chart/transaction-by-barber.js @@ -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', + }, + } + } +}); diff --git a/public/assets/admin/js/filter.js b/public/assets/admin/js/filter.js new file mode 100644 index 0000000..8c33913 --- /dev/null +++ b/public/assets/admin/js/filter.js @@ -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) { }, + }); + }); +}); diff --git a/public/assets/admin/js/random-color.js b/public/assets/admin/js/random-color.js new file mode 100644 index 0000000..9fca188 --- /dev/null +++ b/public/assets/admin/js/random-color.js @@ -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)`; +} diff --git a/resources/views/pages/admin/dashboard/analytic.blade.php b/resources/views/pages/admin/dashboard/analytic.blade.php new file mode 100644 index 0000000..d4b9f39 --- /dev/null +++ b/resources/views/pages/admin/dashboard/analytic.blade.php @@ -0,0 +1,209 @@ +@extends('layouts.admin') +@section('styles') + +@endsection +@section('app') +
+
+
+
+ +
+ To +
+ +
+ +
+
+
+
+
+
+ Total Aset +
+

{{ formatToRupiah($totalAsset) }}

+
+
+
+
+
+
+
+
    +
  • +
    +
    + + + + +
    +
    +
    +
    {{ $totalBarber }}
    + + + Barber + +
    +
  • +
  • +
    +
    + + + + +
    +
    +
    +
    {{ $totalUser }}
    + + + Semua + +
    +
  • +
+
+
+
+
+ Top Barber +

+ {{ $topBarber->biography->full_name ?? '-deleted-' }} +

+ {{ formatToRupiah($topBarber->transactions_sum_total) }} +
+ mobile with coin +
+
+
+
+
+
+
+
Grafik Keuangan
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
Top Layanan
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
Metode Pembayaran
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
Pendapatan By Barber
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
Transaksi By Barber
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+@endsection +@section('beforeScripts') + + +@endsection +@section('afterScripts') + + + + + + + + + +@endsection diff --git a/resources/views/partials/admin/_sidebar.blade.php b/resources/views/partials/admin/_sidebar.blade.php index 74f4c4e..5802048 100644 --- a/resources/views/partials/admin/_sidebar.blade.php +++ b/resources/views/partials/admin/_sidebar.blade.php @@ -49,6 +49,13 @@ Ringkasan +