623 lines
28 KiB
PHP
623 lines
28 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Studio\Dashboard;
|
|
|
|
use App\Models\Bottle;
|
|
use App\Models\Brand;
|
|
use App\Models\Category;
|
|
use App\Models\Customer;
|
|
use App\Models\Expense;
|
|
use App\Models\Order;
|
|
use App\Models\OrderItem;
|
|
use App\Models\Outlet;
|
|
use App\Models\Payment;
|
|
use App\Models\Payroll;
|
|
use App\Models\Perfume;
|
|
use App\Models\Product;
|
|
use App\Models\Purchase;
|
|
use App\Models\Tier;
|
|
use App\Models\User;
|
|
use App\Models\Voucher;
|
|
use App\Traits\Notification\WithSubscribeNotification;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
#[Title('Analisa')]
|
|
class Analysis extends Component
|
|
{
|
|
use WithSubscribeNotification;
|
|
|
|
public string $period = 'all';
|
|
|
|
public array $outlets = [];
|
|
|
|
public array $range = [
|
|
'start' => null,
|
|
'end' => null,
|
|
];
|
|
|
|
public array $selectedOutletIds = [];
|
|
|
|
public array $revenueTrendChart = [];
|
|
|
|
public array $hourlySalesChart = [];
|
|
|
|
public array $categoryDistributionChart = [];
|
|
|
|
public array $paymentMethodChart = [];
|
|
|
|
public array $outletPerformanceChart = [];
|
|
|
|
public array $memberGrowthChart = [];
|
|
|
|
public array $dayOfWeekSalesChart = [];
|
|
|
|
public array $customerTypeChart = [];
|
|
|
|
public array $transactionTrendChart = [];
|
|
|
|
public array $aovTrendChart = [];
|
|
|
|
public array $expenseBreakdownChart = [];
|
|
|
|
public array $revenueVsCostChart = [];
|
|
|
|
public array $customerRetentionChart = [];
|
|
|
|
public array $voucherUsageTrendChart = [];
|
|
|
|
public array $countingData = [];
|
|
|
|
public array $orders = [];
|
|
|
|
public array $expenses = [];
|
|
|
|
public array $topPerfumes = [];
|
|
|
|
public array $topProducts = [];
|
|
|
|
public array $topBottles = [];
|
|
|
|
public function mount(): void
|
|
{
|
|
/** @var \App\Models\User $user */
|
|
$user = auth()->user();
|
|
$this->outlets = $user->outlets()->get()->pluck('name', 'id')->toArray();
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
/** @var \App\Models\User $user */
|
|
$user = auth()->user();
|
|
$outletIds = $this->selectedOutletIds;
|
|
$period = $this->period;
|
|
$range = $this->range;
|
|
|
|
$dateQuery = match ($period) {
|
|
'7' => now()->subDays(7),
|
|
'30' => now()->subDays(30),
|
|
'90' => now()->subDays(90),
|
|
'365' => now()->subDays(365),
|
|
default => null,
|
|
};
|
|
|
|
$startDate = ! empty($range['start']) ? Carbon::parse($range['start'])->startOfDay() : null;
|
|
$endDate = ! empty($range['end']) ? Carbon::parse($range['end'])->endOfDay() : null;
|
|
|
|
if ($startDate && $endDate) {
|
|
$dateQuery = null;
|
|
}
|
|
|
|
$filterDate = function ($q) use ($dateQuery, $startDate, $endDate) {
|
|
if ($startDate && $endDate) {
|
|
$q->whereBetween('created_at', [$startDate, $endDate]);
|
|
} elseif ($dateQuery) {
|
|
$q->where('created_at', '>=', $dateQuery);
|
|
}
|
|
};
|
|
|
|
$countingData = [
|
|
[
|
|
'title' => 'Outlet',
|
|
'value' => Outlet::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
|
|
'description' => 'Total jumlah outlet atau cabang toko yang terdaftar.',
|
|
],
|
|
[
|
|
'title' => 'Pegawai',
|
|
'value' => User::whereHas('employee')
|
|
->when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->withoutDeveloper()
|
|
->count(),
|
|
'description' => 'Jumlah karyawan aktif yang terdaftar di outlet terpilih.',
|
|
],
|
|
[
|
|
'title' => 'Tier',
|
|
'value' => Tier::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
|
|
'description' => 'Jumlah tingkatan loyalitas member (seperti Bronze, Silver, Gold).',
|
|
],
|
|
[
|
|
'title' => 'Voucher Aktif',
|
|
'value' => Voucher::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->active()
|
|
->count(),
|
|
'description' => 'Jumlah kode promo atau voucher yang masih berlaku saat ini.',
|
|
],
|
|
[
|
|
'title' => 'Member',
|
|
'value' => User::whereHas('customer')
|
|
->when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->count(),
|
|
'description' => 'Total pelanggan yang sudah mendaftar sebagai member resmi.',
|
|
],
|
|
[
|
|
'title' => 'Customer',
|
|
'value' => Customer::whereDoesntHave('user')
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->count(),
|
|
'description' => 'Jumlah pelanggan umum (guest) yang pernah melakukan transaksi.',
|
|
],
|
|
[
|
|
'title' => 'Kategori',
|
|
'value' => Category::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
|
|
'description' => 'Jumlah kategori barang (seperti Parfum, Sabun, dll).',
|
|
],
|
|
[
|
|
'title' => 'Merek',
|
|
'value' => Brand::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
|
|
'description' => 'Jumlah merek parfum yang terdaftar di sistem.',
|
|
],
|
|
[
|
|
'title' => 'Parfum',
|
|
'value' => Perfume::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->count(),
|
|
'description' => 'Jumlah varian bibit parfum yang tersedia dalam katalog.',
|
|
],
|
|
[
|
|
'title' => 'Produk',
|
|
'value' => Product::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->count(),
|
|
'description' => 'Jumlah item produk jadi (seperti aksesoris) yang terdaftar.',
|
|
],
|
|
[
|
|
'title' => 'Botol',
|
|
'value' => Bottle::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->count(),
|
|
'description' => 'Jumlah jenis botol parfum yang tersedia.',
|
|
],
|
|
];
|
|
|
|
$orderQuery = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
|
|
$totalIncome = (clone $orderQuery)->sum('total');
|
|
$totalCogs = (clone $orderQuery)->sum('cogs');
|
|
$totalDiscount = (clone $orderQuery)->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
|
|
|
|
$expenseQuery = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
$totalExpense = $expenseQuery->sum('amount');
|
|
|
|
$payrollQuery = Payroll::when(! empty($outletIds), fn($q) => $q->whereHas('user.outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->paid();
|
|
$totalPayroll = $payrollQuery->sum('total_salary');
|
|
|
|
$grossProfit = $totalIncome - $totalCogs - $totalDiscount;
|
|
$netProfit = $grossProfit - $totalExpense - $totalPayroll;
|
|
|
|
$totalOrdersCount = (clone $orderQuery)->count();
|
|
$totalItemsCount = OrderItem::whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))->sum('quantity');
|
|
|
|
$aov = $totalOrdersCount > 0 ? $totalIncome / $totalOrdersCount : 0;
|
|
$profitMargin = $totalIncome > 0 ? ($netProfit / $totalIncome) * 100 : 0;
|
|
$itemsPerOrder = $totalOrdersCount > 0 ? $totalItemsCount / $totalOrdersCount : 0;
|
|
|
|
$customerOrders = (clone $orderQuery)->select('customer_id', DB::raw('count(*) as count'))
|
|
->whereNotNull('customer_id')
|
|
->groupBy('customer_id')
|
|
->get();
|
|
$repeatCustomers = $customerOrders->filter(fn($c) => $c->count > 1)->count();
|
|
$totalCustomers = $customerOrders->count();
|
|
$loyaltyRate = $totalCustomers > 0 ? ($repeatCustomers / $totalCustomers) * 100 : 0;
|
|
|
|
$orders = array_filter([
|
|
[
|
|
'title' => 'Average Order Value (AOV)',
|
|
'value' => formatCurrencyNumber($aov, 'Rp'),
|
|
'description' => 'Rata-rata nilai belanja pelanggan dalam satu kali checkout.',
|
|
],
|
|
$user->hasRole(['Developer', 'Owner']) ? [
|
|
'title' => 'Profit Margin',
|
|
'value' => round($profitMargin, 1) . '%',
|
|
'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.',
|
|
] : null,
|
|
[
|
|
'title' => 'Loyalty Rate',
|
|
'value' => round($loyaltyRate, 1) . '%',
|
|
'description' => 'Persentase pelanggan yang melakukan pembelian lebih dari satu kali.',
|
|
],
|
|
[
|
|
'title' => 'Item per Transaksi',
|
|
'value' => round($itemsPerOrder, 1),
|
|
'description' => 'Rata-rata jumlah barang yang dibeli dalam satu transaksi.',
|
|
],
|
|
[
|
|
'title' => 'Total Order',
|
|
'value' => formatCurrencyNumber($totalOrdersCount),
|
|
'description' => 'Total frekuensi transaksi atau pesanan yang masuk selama periode ini.',
|
|
],
|
|
[
|
|
'title' => 'Pendapatan',
|
|
'value' => formatCurrencyNumber($totalIncome, 'Rp'),
|
|
'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).',
|
|
],
|
|
|
|
$user->hasRole(['Developer', 'Owner']) ? [
|
|
'title' => 'HPP',
|
|
'value' => formatCurrencyNumber($totalCogs, 'Rp'),
|
|
'description' => 'Total modal barang atau Harga Pokok Penjualan (HPP).',
|
|
] : null,
|
|
|
|
$user->hasRole(['Developer', 'Owner']) ? [
|
|
'title' => 'Laba Kotor',
|
|
'value' => formatCurrencyNumber($grossProfit, 'Rp'),
|
|
'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.',
|
|
] : null,
|
|
|
|
$user->hasRole(['Developer', 'Owner']) ? [
|
|
'title' => 'Laba Bersih',
|
|
'value' => formatCurrencyNumber($netProfit, 'Rp'),
|
|
'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.',
|
|
] : null,
|
|
|
|
[
|
|
'title' => 'Diskon',
|
|
'value' => formatCurrencyNumber($totalDiscount, 'Rp'),
|
|
'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.',
|
|
],
|
|
[
|
|
'title' => 'Voucher Terpakai',
|
|
'value' => formatCurrencyNumber(
|
|
(clone $orderQuery)->whereNotNull('voucher_id')->count()
|
|
),
|
|
'description' => 'Jumlah transaksi yang menggunakan voucher diskon selama periode ini.',
|
|
],
|
|
[
|
|
'title' => 'Parfum Terjual',
|
|
'value' => formatCurrencyNumber(
|
|
OrderItem::where('orderable_type', Perfume::class)
|
|
->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
|
|
->sum('quantity')
|
|
) . ' ml',
|
|
'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).',
|
|
],
|
|
[
|
|
'title' => 'Produk Terjual',
|
|
'value' => formatCurrencyNumber(
|
|
OrderItem::where('orderable_type', Product::class)
|
|
->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
|
|
->sum('quantity')
|
|
) . ' pcs',
|
|
'description' => 'Total jumlah unit produk yang terjual dalam satuan pcs.',
|
|
],
|
|
[
|
|
'title' => 'Botol Terjual',
|
|
'value' => formatCurrencyNumber(
|
|
OrderItem::where('orderable_type', Bottle::class)
|
|
->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
|
|
->sum('quantity')
|
|
) . ' pcs',
|
|
'description' => 'Total jumlah unit botol yang terjual dalam satuan pcs.',
|
|
],
|
|
]);
|
|
|
|
$expenses = array_filter([
|
|
[
|
|
'title' => 'Beban Toko',
|
|
'value' => formatCurrencyNumber(
|
|
Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->sum('amount'),
|
|
'Rp'
|
|
),
|
|
'description' => 'Total biaya operasional atau pengeluaran toko yang tercatat selama periode ini.',
|
|
],
|
|
|
|
$user->hasRole(['Developer', 'Owner']) ? [
|
|
'title' => 'Belanja Barang',
|
|
'value' => formatCurrencyNumber(
|
|
Purchase::when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->sum('total'),
|
|
'Rp'
|
|
),
|
|
'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.',
|
|
] : null,
|
|
|
|
[
|
|
'title' => 'Penggajian',
|
|
'value' => formatCurrencyNumber(
|
|
Payroll::when(! empty($outletIds), fn($q) => $q->whereHas('user.outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->paid()
|
|
->sum('total_salary'),
|
|
'Rp'
|
|
),
|
|
'description' => 'Total pengeluaran untuk gaji karyawan.',
|
|
],
|
|
]);
|
|
|
|
$topPerfumes = OrderItem::where('orderable_type', Perfume::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})
|
|
->with('orderable')
|
|
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'), DB::raw('SUM(unit_price * quantity) as total_sales_value'))
|
|
->groupBy('orderable_id', 'orderable_type')
|
|
->orderByDesc('total_sales_value')
|
|
->take(5)
|
|
->get()
|
|
->map(fn($item) => [
|
|
'name' => $item->orderable?->name ?? 'Unknown',
|
|
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
|
|
'total_sold' => formatCurrencyNumber($item->total_sold),
|
|
'total_sales' => formatCurrencyNumber($item->total_sales_value, 'Rp'),
|
|
])
|
|
->toArray();
|
|
|
|
$topProducts = OrderItem::where('orderable_type', Product::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})
|
|
->with('orderable')
|
|
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'), DB::raw('SUM(unit_price * quantity) as total_sales_value'))
|
|
->groupBy('orderable_id', 'orderable_type')
|
|
->orderByDesc('total_sales_value')
|
|
->take(5)
|
|
->get()
|
|
->map(fn($item) => [
|
|
'name' => $item->orderable?->name ?? 'Unknown',
|
|
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
|
|
'total_sold' => formatCurrencyNumber($item->total_sold),
|
|
'total_sales' => formatCurrencyNumber($item->total_sales_value, 'Rp'),
|
|
])
|
|
->toArray();
|
|
|
|
$topBottles = OrderItem::where('orderable_type', Bottle::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})
|
|
->with('orderable')
|
|
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'), DB::raw('SUM(unit_price * quantity) as total_sales_value'))
|
|
->groupBy('orderable_id', 'orderable_type')
|
|
->orderByDesc('total_sales_value')
|
|
->take(5)
|
|
->get()
|
|
->map(fn($item) => [
|
|
'name' => $item->orderable?->name ?? 'Unknown',
|
|
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
|
|
'total_sold' => formatCurrencyNumber($item->total_sold),
|
|
'total_sales' => formatCurrencyNumber($item->total_sales_value, 'Rp'),
|
|
])
|
|
->toArray();
|
|
|
|
// --- Chart Data Logic ---
|
|
|
|
// 1. Revenue & Profit Trend
|
|
$trendDates = collect(range(29, 0))->map(fn($i) => now()->subDays($i)->format('Y-m-d'));
|
|
$ordersTrend = Order::query()
|
|
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->where('created_at', '>=', now()->subDays(30)->startOfDay())
|
|
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(total) as revenue'), DB::raw('SUM(total) - SUM(cogs) - SUM(discount) - SUM(COALESCE(voucher_discount,0)) as gross_profit'), DB::raw('count(*) as count'))
|
|
->groupBy('date')
|
|
->get()
|
|
->keyBy('date');
|
|
|
|
$expensesTrend = Expense::query()
|
|
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->where('created_at', '>=', now()->subDays(30)->startOfDay())
|
|
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total_expense'))
|
|
->groupBy('date')
|
|
->get()
|
|
->keyBy('date');
|
|
|
|
$this->revenueTrendChart = [
|
|
'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
|
|
'revenue' => $trendDates->map(fn($d) => (int) ($ordersTrend->get($d)?->revenue ?? 0))->toArray(),
|
|
'profit' => $trendDates->map(fn($d) => (int) (($ordersTrend->get($d)?->gross_profit ?? 0) - ($expensesTrend->get($d)?->total_expense ?? 0)))->toArray(),
|
|
];
|
|
|
|
// 2. busiest hour
|
|
$hourFunc = DB::getDriverName() === 'sqlite' ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)';
|
|
$hourlySales = (clone $orderQuery)
|
|
->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count'))
|
|
->groupBy('hour')
|
|
->orderBy('hour')
|
|
->get()
|
|
->keyBy('hour');
|
|
|
|
$this->hourlySalesChart = [
|
|
'labels' => collect(range(0, 23))->map(fn($h) => str_pad($h, 2, '0', STR_PAD_LEFT) . ':00')->toArray(),
|
|
'data' => collect(range(0, 23))->map(fn($h) => $hourlySales->get($h)?->count ?? 0)->toArray(),
|
|
];
|
|
|
|
// 3. Product Category Distribution
|
|
$perfumeSales = OrderItem::where('orderable_type', Perfume::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})->sum(DB::raw('unit_price * quantity'));
|
|
|
|
$productSales = OrderItem::where('orderable_type', Product::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})->sum(DB::raw('unit_price * quantity'));
|
|
|
|
$bottleSales = OrderItem::where('orderable_type', Bottle::class)
|
|
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})->sum(DB::raw('unit_price * quantity'));
|
|
|
|
$this->categoryDistributionChart = [
|
|
'labels' => ['Parfum', 'Produk', 'Botol'],
|
|
'data' => [(int) $perfumeSales, (int) $productSales, (int) $bottleSales],
|
|
];
|
|
|
|
// 4. Payment Method
|
|
$paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
|
|
$q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate);
|
|
})
|
|
->select('method', DB::raw('SUM(amount) as total'))
|
|
->groupBy('method')
|
|
->get();
|
|
|
|
$this->paymentMethodChart = [
|
|
'labels' => $paymentMethods->map(fn($pm) => $pm->method?->label() ?? 'Unknown')->toArray(),
|
|
'data' => $paymentMethods->pluck('total')->toArray(),
|
|
];
|
|
|
|
// 5. Outlet Performance
|
|
$outletPerformance = Order::query()
|
|
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->when($dateQuery || ($startDate && $endDate), $filterDate)
|
|
->select('outlet_id', DB::raw('SUM(total) as revenue'))
|
|
->groupBy('outlet_id')
|
|
->with('outlet:id,name')
|
|
->get();
|
|
|
|
$this->outletPerformanceChart = [
|
|
'labels' => $outletPerformance->map(fn($op) => $op->outlet?->name ?? 'Outlet #' . $op->outlet_id)->toArray(),
|
|
'data' => $outletPerformance->pluck('revenue')->toArray(),
|
|
];
|
|
|
|
// 6. Member Growth (Area Chart) - Cumulative
|
|
$memberGrowth = User::whereHas('customer')
|
|
->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count'))
|
|
->groupBy('date')
|
|
->orderBy('date')
|
|
->get();
|
|
|
|
$cumulativeCount = 0;
|
|
$memberChartData = $memberGrowth->map(function ($item) use (&$cumulativeCount) {
|
|
$cumulativeCount += $item->count;
|
|
|
|
return [
|
|
'date' => Carbon::parse($item->date)->format('d M Y'),
|
|
'count' => $cumulativeCount,
|
|
];
|
|
});
|
|
|
|
$this->memberGrowthChart = [
|
|
'labels' => $memberChartData->pluck('date')->toArray(),
|
|
'data' => $memberChartData->pluck('count')->toArray(),
|
|
];
|
|
|
|
// 7. Day of Week Sales
|
|
$dayOfWeekFunc = DB::getDriverName() === 'sqlite' ? "(CAST(strftime('%w', created_at) AS INTEGER) + 1)" : 'DAYOFWEEK(created_at)';
|
|
$dayOfWeekSales = (clone $orderQuery)
|
|
->select(DB::raw("$dayOfWeekFunc as day"), DB::raw('SUM(total) as revenue'))
|
|
->groupBy('day')
|
|
->get()
|
|
->keyBy('day');
|
|
|
|
$days = [
|
|
1 => 'Minggu',
|
|
2 => 'Senin',
|
|
3 => 'Selasa',
|
|
4 => 'Rabu',
|
|
5 => 'Kamis',
|
|
6 => 'Jumat',
|
|
7 => 'Sabtu',
|
|
];
|
|
|
|
$this->dayOfWeekSalesChart = [
|
|
'labels' => array_values($days),
|
|
'data' => collect(range(1, 7))->map(fn($d) => (int) ($dayOfWeekSales->get($d)?->revenue ?? 0))->toArray(),
|
|
];
|
|
|
|
// 8. Customer Type (Member vs Guest)
|
|
$memberOrders = (clone $orderQuery)->whereNotNull('customer_id')->count();
|
|
$guestOrders = (clone $orderQuery)->whereNull('customer_id')->count();
|
|
|
|
$this->customerTypeChart = [
|
|
'labels' => ['Member', 'Guest (Umum)'],
|
|
'data' => [$memberOrders, $guestOrders],
|
|
];
|
|
|
|
// 9. Transaction Trend (Daily Count)
|
|
$this->transactionTrendChart = [
|
|
'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
|
|
'data' => $trendDates->map(fn($d) => (int) ($ordersTrend->get($d)?->count ?? 0))->toArray(),
|
|
];
|
|
|
|
// 10. AOV Trend (Daily Revenue / Daily Count)
|
|
$this->aovTrendChart = [
|
|
'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
|
|
'data' => $trendDates->map(function ($d) use ($ordersTrend) {
|
|
$rev = $ordersTrend->get($d)?->revenue ?? 0;
|
|
$count = $ordersTrend->get($d)?->count ?? 0;
|
|
|
|
return $count > 0 ? (int) ($rev / $count) : 0;
|
|
})->toArray(),
|
|
];
|
|
|
|
// 11. Expense Breakdown (Operational vs Payroll)
|
|
$this->expenseBreakdownChart = [
|
|
'labels' => ['Beban Operasional', 'Gaji (Payroll)'],
|
|
'data' => [(int) $totalExpense, (int) $totalPayroll],
|
|
];
|
|
|
|
// 12. Revenue vs Total Cost (COGS + Expense + Payroll)
|
|
$this->revenueVsCostChart = [
|
|
'labels' => ['Pendapatan', 'Total Biaya (HPP + Beban + Gaji)'],
|
|
'data' => [(int) $totalIncome, (int) ($totalCogs + $totalExpense + $totalPayroll)],
|
|
];
|
|
|
|
// 13. Customer Retention (New vs Returning Transactions)
|
|
$this->customerRetentionChart = [
|
|
'labels' => ['Pelanggan Setia (Repeat)', 'Pelanggan Baru/Sekali'],
|
|
'data' => [$repeatCustomers, max(0, $totalCustomers - $repeatCustomers)],
|
|
];
|
|
|
|
// 14. Voucher Usage Trend
|
|
$vouchersTrend = Order::query()
|
|
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
|
|
->where('created_at', '>=', now()->subDays(30)->startOfDay())
|
|
->whereNotNull('voucher_id')
|
|
->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count'))
|
|
->groupBy('date')
|
|
->get()
|
|
->keyBy('date');
|
|
|
|
$this->voucherUsageTrendChart = [
|
|
'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
|
|
'data' => $trendDates->map(fn($d) => (int) ($vouchersTrend->get($d)?->count ?? 0))->toArray(),
|
|
];
|
|
|
|
$this->countingData = $countingData;
|
|
$this->orders = array_values(array_filter($orders));
|
|
$this->expenses = array_values(array_filter($expenses));
|
|
$this->topPerfumes = $topPerfumes;
|
|
$this->topProducts = $topProducts;
|
|
$this->topBottles = $topBottles;
|
|
|
|
return view('livewire.studio.dashboard.analysis', [
|
|
'pageTitle' => 'Analisa',
|
|
]);
|
|
}
|
|
}
|