parfum/app/Livewire/Studio/Dashboard/Overview.php

447 lines
20 KiB
PHP

<?php
namespace App\Livewire\Studio\Dashboard;
use App\Models\Bottle;
use App\Models\Expense;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payment;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\Purchase;
use App\Models\User;
use App\Traits\Notification\WithSubscribeNotification;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Ringkasan')]
class Overview extends Component
{
use WithSubscribeNotification;
public array $outlets = [];
public array $selectedOutletIds = [];
public array $hourlyComparisonChart = [];
public array $todayCategoryChart = [];
public array $todayPaymentChart = [];
public array $todayCustomerTypeChart = [];
public array $todayTopSellingChart = [];
public array $todayTopCustomersChart = [];
public array $todayDiscountChart = [];
public array $stats = [];
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;
if (count($this->outlets) === 1) {
$outletIds = $user->outlets()->pluck('outlets.id')->toArray();
}
$todayOrder = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->count();
$yesterdayOrder = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->count();
$todayIncome = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->sum('total');
$yesterdayIncome = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->sum('total');
$todayCogs = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->sum('cogs');
$yesterdayCogs = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->sum('cogs');
$todayDiscount = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
$yesterdayDiscount = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
$todayVoucher = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->whereNotNull('voucher_id')
->count();
$yesterdayVoucher = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->whereNotNull('voucher_id')
->count();
$todayExpense = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today()
->sum('amount');
$yesterdayExpense = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday()
->sum('amount');
$todayPurchase = Purchase::query()->today()->sum('total');
$yesterdayPurchase = Purchase::query()->yesterday()->sum('total');
$todayGrossProfit = $todayIncome - $todayCogs - $todayDiscount;
$yesterdayGrossProfit = $yesterdayIncome - $yesterdayCogs - $yesterdayDiscount;
$todayNetProfit = $todayGrossProfit - $todayExpense;
$yesterdayNetProfit = $yesterdayGrossProfit - $yesterdayExpense;
$todaySoldPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas(
'order',
fn($q) => $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
)
->sum('quantity');
$yesterdaySoldPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas(
'order',
fn($q) => $q->yesterday()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
)
->sum('quantity');
$todayTopPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas(
'order',
fn($q) => $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
)
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
->groupBy('orderable_id', 'orderable_type')
->orderByDesc('total_sold')
->with('orderable')
->first();
$yesterdayTopPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas(
'order',
fn($q) => $q->yesterday()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
)
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
->groupBy('orderable_id', 'orderable_type')
->orderByDesc('total_sold')
->with('orderable')
->first();
// New Card: Members Joined
$todayNewMembers = User::whereHas('customer')->whereDate('created_at', now())->count();
$yesterdayNewMembers = User::whereHas('customer')->whereDate('created_at', now()->yesterday())->count();
// New Card: AOV
$todayAov = $todayOrder > 0 ? $todayIncome / $todayOrder : 0;
$yesterdayAov = $yesterdayOrder > 0 ? $yesterdayIncome / $yesterdayOrder : 0;
$todayProfitMargin = $todayIncome > 0 ? ($todayNetProfit / $todayIncome) * 100 : 0;
$yesterdayProfitMargin = $yesterdayIncome > 0 ? ($yesterdayNetProfit / $yesterdayIncome) * 100 : 0;
// --- Chart Data Logic ---
// 1. Hourly Sales Comparison (Today vs Yesterday)
$hourFunc = DB::getDriverName() === 'sqlite' ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)';
$todayHourly = Order::whereDate('created_at', now())
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count'))
->groupBy('hour')
->get()->keyBy('hour');
$yesterdayHourly = Order::whereDate('created_at', now()->yesterday())
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count'))
->groupBy('hour')
->get()->keyBy('hour');
$this->hourlyComparisonChart = [
'labels' => collect(range(0, 23))->map(fn($h) => str_pad($h, 2, '0', STR_PAD_LEFT) . ':00')->toArray(),
'today' => collect(range(0, 23))->map(fn($h) => $todayHourly->get($h)?->count ?? 0)->toArray(),
'yesterday' => collect(range(0, 23))->map(fn($h) => $yesterdayHourly->get($h)?->count ?? 0)->toArray(),
];
// 2. Today's Distribution (Category)
$categorySales = OrderItem::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
})
->select('orderable_type', DB::raw('SUM(unit_price * quantity) as total'))
->groupBy('orderable_type')
->get()->keyBy('orderable_type');
$this->todayCategoryChart = [
'labels' => ['Parfum', 'Produk', 'Botol'],
'data' => [
(int) ($categorySales->get(Perfume::class)?->total ?? 0),
(int) ($categorySales->get(Product::class)?->total ?? 0),
(int) ($categorySales->get(Bottle::class)?->total ?? 0),
],
];
// 3. Today's Payment Methods
$paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
})
->select('method', DB::raw('SUM(amount) as total'))
->groupBy('method')
->get();
$this->todayPaymentChart = [
'labels' => $paymentMethods->map(fn($pm) => $pm->method?->label() ?? 'Unknown')->toArray(),
'data' => $paymentMethods->pluck('total')->toArray(),
];
// 4. Today's Customer Type
$todayMemberOrders = Order::query()
->today()
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNotNull('customer_id')->count();
$todayGuestOrders = Order::query()
->today()
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNull('customer_id')->count();
$this->todayCustomerTypeChart = [
'labels' => ['Member', 'Guest (Umum)'],
'data' => [$todayMemberOrders, $todayGuestOrders],
];
// 5. Today's Top 5 Selling Items
$todayTopItems = OrderItem::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
})
->with('orderable')
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
->groupBy('orderable_id', 'orderable_type')
->orderByDesc('total_sold')
->take(5)
->get();
$this->todayTopSellingChart = [
'labels' => $todayTopItems->map(fn($item) => $item->orderable?->name ?? 'Unknown')->toArray(),
'data' => $todayTopItems->pluck('total_sold')->map(fn($val) => (int) $val)->toArray(),
];
// 6. Today's Top 5 Customers
$todayTopCustomers = Order::query()
->today()
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNotNull('customer_id')
->select('customer_id', DB::raw('SUM(total) as total_spent'))
->groupBy('customer_id')
->orderByDesc('total_spent')
->take(5)
->with('customer')
->get();
$this->todayTopCustomersChart = [
'labels' => $todayTopCustomers->map(fn($o) => $o->customer?->user?->employee?->full_name ?? $o->customer?->user?->name ?? 'Customer #' . $o->customer_id)->toArray(),
'data' => $todayTopCustomers->pluck('total_spent')->map(fn($val) => (int) $val)->toArray(),
];
// 7. Today's Discount Distribution (With Discount vs Regular)
$withDiscount = Order::query()
->today()
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('discount', '>', 0)->count();
$noDiscount = Order::query()
->today()
->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('discount', '<=', 0)->count();
$this->todayDiscountChart = [
'labels' => ['Pakai Diskon', 'Harga Normal'],
'data' => [$withDiscount, $noDiscount],
];
$this->stats = array_values(array_filter([
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Average Order Value (AOV)',
'value' => formatCurrencyNumber($todayAov, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayAov, 'Rp'),
'trend' => $yesterdayAov > 0
? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayAov > $yesterdayAov,
'description' => 'Rata-rata nilai belanja pelanggan dalam satu kali checkout.',
] : null,
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Profit Margin',
'value' => round($todayProfitMargin, 1) . '%',
'previous' => round($yesterdayProfitMargin, 1) . '%',
'trend' => $yesterdayProfitMargin > 0
? round($todayProfitMargin - $yesterdayProfitMargin, 1) . '%'
: '∞%',
'trendUp' => $todayProfitMargin > $yesterdayProfitMargin,
'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.',
] : null,
[
'title' => 'Total Order',
'value' => $todayOrder,
'previous' => $yesterdayOrder,
'trend' => $yesterdayOrder > 0
? round((($todayOrder - $yesterdayOrder) / $yesterdayOrder) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayOrder > $yesterdayOrder,
'description' => 'Total frekuensi transaksi atau pesanan yang masuk selama periode ini.',
],
[
'title' => 'Pendapatan',
'value' => formatCurrencyNumber($todayIncome, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayIncome, 'Rp'),
'trend' => $yesterdayIncome > 0
? round((($todayIncome - $yesterdayIncome) / $yesterdayIncome) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayIncome > $yesterdayIncome,
'formatted' => formatCurrencyNumber($todayIncome, 'Rp'),
'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).',
],
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'HPP',
'value' => formatCurrencyNumber($todayCogs, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayCogs, 'Rp'),
'trend' => $yesterdayCogs > 0
? round((($todayCogs - $yesterdayCogs) / $yesterdayCogs) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayCogs > $yesterdayCogs,
'formatted' => formatCurrencyNumber($todayCogs, 'Rp'),
'description' => 'Harga Pokok Penjualan, yaitu total modal dari barang-barang yang terjual.',
] : null,
[
'title' => 'Member Baru',
'value' => $todayNewMembers . ' Orang',
'previous' => $yesterdayNewMembers . ' Orang',
'trend' => $yesterdayNewMembers > 0
? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayNewMembers > $yesterdayNewMembers,
'description' => 'Jumlah pelanggan baru yang mendaftar menjadi member selama periode ini.',
],
[
'title' => 'Diskon',
'value' => formatCurrencyNumber($todayDiscount, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayDiscount, 'Rp'),
'trend' => $yesterdayDiscount > 0
? round((($todayDiscount - $yesterdayDiscount) / $yesterdayDiscount) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayDiscount > $yesterdayDiscount,
'formatted' => formatCurrencyNumber($todayDiscount, 'Rp'),
'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.',
],
[
'title' => 'Voucher Terpakai',
'value' => $todayVoucher,
'previous' => $yesterdayVoucher,
'trend' => $yesterdayVoucher > 0
? round((($todayVoucher - $yesterdayVoucher) / $yesterdayVoucher) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayVoucher > $yesterdayVoucher,
'description' => 'Jumlah transaksi yang menggunakan voucher diskon selama periode ini.',
],
[
'title' => 'Beban Toko',
'value' => formatCurrencyNumber($todayExpense, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayExpense, 'Rp'),
'trend' => $yesterdayExpense > 0
? round((($todayExpense - $yesterdayExpense) / $yesterdayExpense) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayExpense > $yesterdayExpense,
'description' => 'Total biaya operasional atau pengeluaran toko yang tercatat selama periode ini.',
],
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Belanja Barang',
'value' => formatCurrencyNumber($todayPurchase, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayPurchase, 'Rp'),
'trend' => $yesterdayPurchase > 0
? round((($todayPurchase - $yesterdayPurchase) / $yesterdayPurchase) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayPurchase > $yesterdayPurchase,
'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.',
] : null,
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Kotor',
'value' => formatCurrencyNumber($todayGrossProfit, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayGrossProfit, 'Rp'),
'trend' => $yesterdayGrossProfit > 0
? round((($todayGrossProfit - $yesterdayGrossProfit) / $yesterdayGrossProfit) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayGrossProfit > $yesterdayGrossProfit,
'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.',
] : null,
$user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Bersih',
'value' => formatCurrencyNumber($todayNetProfit, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayNetProfit, 'Rp'),
'trend' => $yesterdayNetProfit > 0
? round((($todayNetProfit - $yesterdayNetProfit) / $yesterdayNetProfit) * 100, 1) . '%'
: '∞%',
'trendUp' => $todayNetProfit > $yesterdayNetProfit,
'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.',
] : null,
[
'title' => 'Parfum Terjual',
'value' => $todaySoldPerfume . ' ml',
'previous' => $yesterdaySoldPerfume . ' ml',
'trend' => $yesterdaySoldPerfume > 0
? round((($todaySoldPerfume - $yesterdaySoldPerfume) / $yesterdaySoldPerfume) * 100, 1) . '%'
: '∞%',
'trendUp' => $todaySoldPerfume > $yesterdaySoldPerfume,
'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).',
],
[
'title' => 'Top Parfum',
'value' => optional($todayTopPerfume?->orderable)->name . ' (' . $todayTopPerfume?->total_sold . 'ml)' ?? '-',
'previous' => optional($yesterdayTopPerfume?->orderable)->name . '(' . $yesterdayTopPerfume?->total_sold . 'ml)' ?? '-',
'trend' => ($yesterdayTopPerfume && $yesterdayTopPerfume->total_sold > 0)
? round((($todayTopPerfume?->total_sold - $yesterdayTopPerfume?->total_sold) / $yesterdayTopPerfume?->total_sold) * 100, 1) . '%'
: '∞%',
'trendUp' => ($todayTopPerfume?->total_sold ?? 0) > ($yesterdayTopPerfume?->total_sold ?? 0),
'description' => 'Nama varian parfum yang paling banyak terjual hari ini.',
],
]));
return view('livewire.studio.dashboard.overview');
}
}