feat: add date filter, multiple new charts, and top customers list to the analysis page.
This commit is contained in:
parent
84909d9392
commit
77c750db40
@ -4,17 +4,101 @@
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DelayedGood;
|
||||
use App\Models\EggCollection;
|
||||
use App\Models\EggCollectionItem;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Feed;
|
||||
use App\Models\FeedPurchase;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payroll;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
|
||||
class Analisys extends Page
|
||||
class Analisys extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'period' => 'this_month',
|
||||
'from_date' => now()->startOfMonth()->format('d F Y'),
|
||||
'to_date' => now()->format('d F Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('period')
|
||||
->label('Periode Cepat')
|
||||
->options([
|
||||
'today' => 'Hari Ini',
|
||||
'this_week' => 'Minggu Ini',
|
||||
'this_month' => 'Bulan Ini',
|
||||
'this_year' => 'Tahun Ini',
|
||||
'custom' => 'Kustom Tanggal',
|
||||
])
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $set, $get) {
|
||||
if ($state === 'today') {
|
||||
$set('from_date', now()->format('d F Y'));
|
||||
$set('to_date', now()->format('d F Y'));
|
||||
} elseif ($state === 'this_week') {
|
||||
$set('from_date', now()->startOfWeek()->format('d F Y'));
|
||||
$set('to_date', now()->endOfWeek()->format('d F Y'));
|
||||
} elseif ($state === 'this_month') {
|
||||
$set('from_date', now()->startOfMonth()->format('d F Y'));
|
||||
$set('to_date', now()->endOfMonth()->format('d F Y'));
|
||||
} elseif ($state === 'this_year') {
|
||||
$set('from_date', now()->startOfYear()->format('d F Y'));
|
||||
$set('to_date', now()->endOfYear()->format('d F Y'));
|
||||
}
|
||||
$this->dispatch('stats-updated');
|
||||
}),
|
||||
|
||||
DatePicker::make('from_date')
|
||||
->label('Dari Tanggal')
|
||||
->native(false)
|
||||
->displayFormat('d F Y')
|
||||
->live()
|
||||
->afterStateUpdated(function ($set) {
|
||||
$set('period', 'custom');
|
||||
$this->dispatch('stats-updated');
|
||||
}),
|
||||
|
||||
DatePicker::make('to_date')
|
||||
->label('Sampai Tanggal')
|
||||
->native(false)
|
||||
->displayFormat('d F Y')
|
||||
->live()
|
||||
->afterStateUpdated(function ($set) {
|
||||
$set('period', 'custom');
|
||||
$this->dispatch('stats-updated');
|
||||
}),
|
||||
]),
|
||||
])
|
||||
->compact(),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartPie;
|
||||
|
||||
protected static ?string $navigationLabel = 'Analisis';
|
||||
@ -28,6 +112,14 @@ class Analisys extends Page
|
||||
public function getViewData(): array
|
||||
{
|
||||
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
|
||||
$now = now();
|
||||
|
||||
$fromDateInput = $this->data['from_date'] ?? now()->startOfMonth()->format('d F Y');
|
||||
$toDateInput = $this->data['to_date'] ?? now()->format('d F Y');
|
||||
|
||||
// Normalize for DB queries
|
||||
$fromDate = Carbon::parse($fromDateInput)->format('Y-m-d');
|
||||
$toDate = Carbon::parse($toDateInput)->format('Y-m-d');
|
||||
|
||||
// 1. Count customers
|
||||
$countPelanggan = Customer::count();
|
||||
@ -37,6 +129,7 @@ public function getViewData(): array
|
||||
->join('egg_collections', 'egg_collection_items.egg_collection_id', '=', 'egg_collections.id')
|
||||
->join('units', 'egg_collection_items.unit_id', '=', 'units.id')
|
||||
->whereNull('egg_collections.deleted_at')
|
||||
->whereBetween('egg_collections.production_date', [$fromDate, $toDate])
|
||||
->selectRaw('units.name as unit_name,
|
||||
sum(case when is_broken = 0 then quantity else 0 end) as total_good,
|
||||
sum(case when is_broken = 1 then quantity else 0 end) as total_broken')
|
||||
@ -49,11 +142,12 @@ public function getViewData(): array
|
||||
]);
|
||||
|
||||
// 3. Count total orders
|
||||
$countPesanan = Order::count();
|
||||
$countPesanan = Order::whereBetween('order_date', [$fromDate, $toDate])->count();
|
||||
|
||||
// 4. Count total delayed goods (by unit)
|
||||
$totalDelayedByUnit = DelayedGood::query()
|
||||
->join('units', 'delayed_goods.unit_id', '=', 'units.id')
|
||||
->whereBetween('delayed_goods.stored_date', [$fromDate, $toDate])
|
||||
->selectRaw('units.name as unit_name, sum(quantity) as total')
|
||||
->groupBy('units.name')
|
||||
->get()
|
||||
@ -62,34 +156,28 @@ public function getViewData(): array
|
||||
'total' => $formatNumber($item->total),
|
||||
]);
|
||||
|
||||
// 5. Revenue (from orders and delayed goods)
|
||||
$pendapatanPesanan = Order::sum('total_amount');
|
||||
$pendapatanTertunda = DelayedGood::sum('total_amount');
|
||||
// 5. Revenue
|
||||
$pendapatanPesanan = Order::whereBetween('order_date', [$fromDate, $toDate])->sum('total_amount');
|
||||
$pendapatanTertunda = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('total_amount');
|
||||
$totalPendapatan = $pendapatanPesanan + $pendapatanTertunda;
|
||||
|
||||
// 6. Expenses
|
||||
$totalPengeluaran = Expense::sum('amount');
|
||||
// 6. Financial totals
|
||||
$totalPengeluaran = Expense::whereBetween('expense_date', [$fromDate, $toDate])->sum('amount');
|
||||
$totalPenggajian = Payroll::whereBetween('period_month', [Carbon::parse($fromDate)->format('Y-m'), Carbon::parse($toDate)->format('Y-m')])->sum('total_salary');
|
||||
$totalBelanjaPakan = FeedPurchase::whereBetween('purchase_date', [$fromDate, $toDate])->sum('total_price');
|
||||
|
||||
// 7. Payroll
|
||||
$totalPenggajian = Payroll::sum('total_salary');
|
||||
|
||||
// 8. Feed purchases
|
||||
$totalBelanjaPakan = FeedPurchase::sum('total_price');
|
||||
|
||||
// 9. Gross profit (Revenue - COGS/Feed)
|
||||
$labaKotor = $totalPendapatan - $totalBelanjaPakan;
|
||||
|
||||
// 10. Net profit (Gross Profit - Expenses - Payroll)
|
||||
$labaBersih = $labaKotor - $totalPengeluaran - $totalPenggajian;
|
||||
|
||||
// 11. Unpaid accounts receivable (delayed goods)
|
||||
$totalPiutang = DelayedGood::sum('total_amount') - DelayedGood::sum('paid_amount');
|
||||
$paidInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('paid_amount');
|
||||
$totalInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('total_amount');
|
||||
$totalPiutang = $totalInRange - $paidInRange;
|
||||
|
||||
$mainStats = [
|
||||
['label' => 'Total Pelanggan', 'desc' => 'Jumlah pelanggan terdaftar', 'value' => $countPelanggan, 'prefix' => ''],
|
||||
['label' => 'Total Pesanan', 'desc' => 'Jumlah transaksi pesanan', 'value' => $countPesanan, 'prefix' => ''],
|
||||
['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi beban operasional', 'value' => $formatNumber($labaBersih), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Piutang', 'desc' => 'Piutang barang tertunda', 'value' => $formatNumber($totalPiutang), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Piutang', 'desc' => 'Piutang barang tertunda (periode)', 'value' => $formatNumber($totalPiutang), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Pendapatan', 'desc' => 'Akumulasi semua pemasukan', 'value' => $formatNumber($totalPendapatan), 'prefix' => 'Rp'],
|
||||
['label' => 'Laba Kotor', 'desc' => 'Pendapatan dikurangi biaya pakan', 'value' => $formatNumber($labaKotor), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Pengeluaran', 'desc' => 'Biaya operasional & umum', 'value' => $formatNumber($totalPengeluaran), 'prefix' => 'Rp'],
|
||||
@ -97,10 +185,117 @@ public function getViewData(): array
|
||||
['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalBelanjaPakan), 'prefix' => 'Rp'],
|
||||
];
|
||||
|
||||
// Top 5 Customers by Order Amount in Range
|
||||
$topCustomers = Customer::query()
|
||||
->withSum(['orders' => fn ($q) => $q->whereBetween('order_date', [$fromDate, $toDate])], 'total_amount')
|
||||
->withSum(['delayedGoods' => fn ($q) => $q->whereBetween('stored_date', [$fromDate, $toDate])], 'total_amount')
|
||||
->get()
|
||||
->map(function ($customer) {
|
||||
$customer->total_spent = ($customer->orders_sum_total_amount ?? 0) + ($customer->delayed_goods_sum_total_amount ?? 0);
|
||||
|
||||
return $customer;
|
||||
})
|
||||
->sortByDesc('total_spent')
|
||||
->take(5)
|
||||
->values();
|
||||
|
||||
// 7. Chart: Daily Production (In range)
|
||||
$diffDays = Carbon::parse($fromDate)->diffInDays(Carbon::parse($toDate));
|
||||
$rangeQueryDays = collect(range(0, min($diffDays, 30)))->map(fn ($i) => Carbon::parse($fromDate)->addDays($i)->format('Y-m-d'))->values();
|
||||
|
||||
$productionChart = [
|
||||
'labels' => $rangeQueryDays->map(fn ($d) => Carbon::parse($d)->format('d M'))->values(),
|
||||
'good' => $rangeQueryDays->map(fn ($d) => (float) EggCollection::whereDate('production_date', $d)->sum('total_eggs'))->values(),
|
||||
'broken' => $rangeQueryDays->map(fn ($d) => (float) EggCollection::whereDate('production_date', $d)->sum('total_broken_eggs'))->values(),
|
||||
];
|
||||
|
||||
// 8. Financial Trends (Monthly - 6 months prior to to_date)
|
||||
$months = collect(range(0, 5))->map(fn ($i) => Carbon::parse($toDate)->subMonths($i)->format('Y-m'))->reverse();
|
||||
$financialChart = [
|
||||
'labels' => $months->map(fn ($m) => Carbon::parse($m)->translatedFormat('M Y'))->values(),
|
||||
'revenue' => $months->map(function ($m) {
|
||||
$orderRev = Order::whereDate('order_date', 'like', "$m%")->sum('total_amount');
|
||||
$delayedRev = DelayedGood::whereDate('stored_date', 'like', "$m%")->sum('total_amount');
|
||||
|
||||
return (float) ($orderRev + $delayedRev);
|
||||
})->values(),
|
||||
'expenses' => $months->map(function ($m) {
|
||||
$exp = Expense::whereDate('expense_date', 'like', "$m%")->sum('amount');
|
||||
$payroll = Payroll::where('period_month', 'like', "$m%")->sum('total_salary');
|
||||
$feed = FeedPurchase::whereDate('purchase_date', 'like', "$m%")->sum('total_price');
|
||||
|
||||
return (float) ($exp + $payroll + $feed);
|
||||
})->values(),
|
||||
];
|
||||
|
||||
// 8b. Chart: Monthly Profit Trend
|
||||
$financialChart['profit'] = collect($financialChart['revenue'])->map(fn ($rev, $i) => (float) ($rev - $financialChart['expenses'][$i]))->values();
|
||||
|
||||
// 9. Chart: Expense Distribution
|
||||
$expenseDistribution = [
|
||||
'labels' => ['Gaji Karyawan', 'Belanja Pakan', 'Pengeluaran Umum'],
|
||||
'data' => [(float) $totalPenggajian, (float) $totalBelanjaPakan, (float) $totalPengeluaran],
|
||||
];
|
||||
|
||||
// 10. Chart: Sales Distribution by Unit
|
||||
$salesByUnit = Order::whereBetween('order_date', [$fromDate, $toDate])
|
||||
->join('units', 'orders.unit_id', '=', 'units.id')
|
||||
->selectRaw('units.name, sum(total_amount) as total')
|
||||
->groupBy('units.name')
|
||||
->get();
|
||||
|
||||
// 11. Chart: Debt Status (In range)
|
||||
$debtStatus = [
|
||||
'paid' => (float) $paidInRange,
|
||||
'unpaid' => (float) ($totalInRange - $paidInRange),
|
||||
];
|
||||
|
||||
// 12. Chart: Feed Stocks (Current)
|
||||
$feedStocks = Feed::query()
|
||||
->join('units', 'feeds.unit_id', '=', 'units.id')
|
||||
->select('feeds.name', 'feeds.stock', 'units.alias as unit_alias')
|
||||
->get();
|
||||
|
||||
// 13. Chart: Production by Warehouse (In range)
|
||||
$productionByWarehouse = EggCollection::whereBetween('production_date', [$fromDate, $toDate])
|
||||
->join('warehouses', 'egg_collections.warehouse_id', '=', 'warehouses.id')
|
||||
->selectRaw('warehouses.name, sum(total_eggs) as total')
|
||||
->groupBy('warehouses.name')
|
||||
->get();
|
||||
|
||||
// 14. Chart: Revenue Split (In range)
|
||||
$revenueSplit = [
|
||||
'orders' => (float) $pendapatanPesanan,
|
||||
'delayed' => (float) $pendapatanTertunda,
|
||||
];
|
||||
|
||||
return [
|
||||
'mainStats' => $mainStats,
|
||||
'totalEggsByUnit' => $totalEggsByUnit,
|
||||
'totalDelayedByUnit' => $totalDelayedByUnit,
|
||||
'topCustomers' => $topCustomers,
|
||||
'productionChart' => $productionChart,
|
||||
'financialChart' => $financialChart,
|
||||
'expenseDistribution' => $expenseDistribution,
|
||||
'salesByUnit' => [
|
||||
'labels' => $salesByUnit->pluck('name')->values(),
|
||||
'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(),
|
||||
],
|
||||
'debtStatus' => $debtStatus,
|
||||
'feedStocks' => [
|
||||
'labels' => $feedStocks->pluck('name')->values(),
|
||||
'data' => $feedStocks->pluck('stock')->map(fn ($v) => (float) $v)->values(),
|
||||
'units' => $feedStocks->pluck('unit_alias')->values(),
|
||||
],
|
||||
'productionByWarehouse' => [
|
||||
'labels' => $productionByWarehouse->pluck('name')->values(),
|
||||
'data' => $productionByWarehouse->pluck('total')->map(fn ($v) => (float) $v)->values(),
|
||||
],
|
||||
'revenueSplit' => [
|
||||
'labels' => ['Penjualan Langsung', 'Barang Tertunda'],
|
||||
'data' => [$revenueSplit['orders'], $revenueSplit['delayed']],
|
||||
],
|
||||
'formatNumber' => $formatNumber,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Customer extends Model
|
||||
@ -11,4 +12,14 @@ class Customer extends Model
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
|
||||
public function delayedGoods(): HasMany
|
||||
{
|
||||
return $this->hasMany(DelayedGood::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<div class="mb-6">
|
||||
<form wire:submit="getViewData">
|
||||
{{ $this->form }}
|
||||
</form>
|
||||
</div>
|
||||
<div class="space-y-6 pb-12">
|
||||
{{-- Main Stats Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
@foreach ($mainStats as $stat)
|
||||
@ -11,32 +16,172 @@ class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-tig
|
||||
<div class="flex items-baseline gap-1">
|
||||
@if ($stat['prefix'])
|
||||
<span
|
||||
class="text-sm font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span>
|
||||
class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span>
|
||||
@endif
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ $stat['value'] }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-600">{{ $stat['desc'] }}</span>
|
||||
<span
|
||||
class="text-[10px] text-gray-400 dark:text-gray-600 italic tracking-wide uppercase">{{ $stat['desc'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- First Row: Production Chart & Expense Distribution --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
class="lg:col-span-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-chart-bar class="w-4 h-4 text-primary-500" />
|
||||
Tren Produksi Telur (7-14 Hari Terakhir)
|
||||
</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="productionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-chart-pie class="w-4 h-4 text-primary-500" />
|
||||
Alokasi Pengeluaran
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="expenseChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Second Row: Financial Trend & Top Customers --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
class="lg:col-span-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-presentation-chart-line class="w-4 h-4 text-primary-500" />
|
||||
Performa Keuangan (6 Bulan Terakhir)
|
||||
</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="financialChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center gap-2">
|
||||
<x-heroicon-o-star class="w-4 h-4 text-amber-500" />
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Top 5 Pelanggan</h3>
|
||||
</div>
|
||||
<div class="flex-1 divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@foreach ($topCustomers as $customer)
|
||||
<div
|
||||
class="px-5 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-800/40 transition">
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm font-bold text-gray-800 dark:text-gray-200">{{ $customer->name }}</span>
|
||||
<span
|
||||
class="text-xs text-gray-400 tracking-tight">{{ $customer->phone ?? 'Telp tidak ada' }}</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span
|
||||
class="text-xs font-semibold text-gray-400 uppercase block leading-none mb-1">Total
|
||||
Belanja</span>
|
||||
<span class="text-sm font-black text-primary-600">Rp
|
||||
{{ $formatNumber($customer->total_spent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Third Row: Sales by Unit & Payment Status --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-6 border-t border-gray-100 dark:border-gray-800">
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-shopping-bag class="w-4 h-4 text-primary-500" />
|
||||
Penjualan Berdasarkan Satuan (Order)
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="salesUnitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-shield-check class="w-4 h-4 text-primary-500" />
|
||||
Status Pelunasan (Barang Tertunda)
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="debtStatusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fourth Row: Feed Stocks --}}
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6 mt-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-circle-stack class="w-4 h-4 text-primary-500" />
|
||||
Level Stok Pakan Saat Ini
|
||||
</h3>
|
||||
<div class="h-80">
|
||||
<canvas id="feedStockChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fifth Row: Profit Trend & Warehouse Productivity --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-6 border-t border-gray-100 dark:border-gray-800">
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-arrow-trending-up class="w-4 h-4 text-emerald-500" />
|
||||
Tren Laba Bersih (6 Bulan Terakhir)
|
||||
</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="profitTrendChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-home-modern class="w-4 h-4 text-primary-500" />
|
||||
Produktivitas per Gudang
|
||||
</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="warehouseChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sixth Row: Revenue Split --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 pt-6">
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
|
||||
<x-heroicon-o-arrows-right-left class="w-4 h-4 text-primary-500" />
|
||||
Sumber Pendapatan
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="revenueSplitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Placeholder/Extra space to balance --}}
|
||||
<div class="lg:col-span-2"></div>
|
||||
</div>
|
||||
|
||||
{{-- Units Breakdown --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Production --}}
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Produksi Telur (By Unit)</h3>
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Total Stok (By Unit)</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@forelse($totalEggsByUnit as $item)
|
||||
<div
|
||||
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="text-right">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase block">Bagus</span>
|
||||
@ -51,7 +196,7 @@ class="text-sm font-bold text-rose-600 dark:text-rose-400">{{ $item['total_broke
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm">Belum ada data produksi</div>
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada data produksi</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@ -71,13 +216,251 @@ class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name
|
||||
<span class="text-sm font-bold text-gray-900 dark:text-white">{{ $item['total'] }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm">Tidak ada barang tertunda</div>
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Tidak ada barang tertunda</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
let chartInstances = {};
|
||||
|
||||
function destroyCharts() {
|
||||
Object.values(chartInstances).forEach(chart => {
|
||||
if (chart) chart.destroy();
|
||||
});
|
||||
chartInstances = {};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
destroyCharts();
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Production Chart
|
||||
const ctxProduction = document.getElementById('productionChart');
|
||||
if (ctxProduction) {
|
||||
chartInstances.production = new Chart(ctxProduction, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {!! json_encode($productionChart['labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Lampu/Bagus',
|
||||
data: {!! json_encode($productionChart['good']) !!},
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: '#10b98122',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Pecah',
|
||||
data: {!! json_encode($productionChart['broken']) !!},
|
||||
borderColor: '#f43f5e',
|
||||
backgroundColor: '#f43f5e22',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Financial Chart
|
||||
const ctxFinancial = document.getElementById('financialChart');
|
||||
if (ctxFinancial) {
|
||||
chartInstances.financial = new Chart(ctxFinancial, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {!! json_encode($financialChart['labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Pendapatan',
|
||||
data: {!! json_encode($financialChart['revenue']) !!},
|
||||
backgroundColor: '#6366f1',
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'Total Biaya',
|
||||
data: {!! json_encode($financialChart['expenses']) !!},
|
||||
backgroundColor: '#94a3b8',
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Expense Category Chart
|
||||
const ctxExpense = document.getElementById('expenseChart');
|
||||
if (ctxExpense) {
|
||||
chartInstances.expense = new Chart(ctxExpense, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($expenseDistribution['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($expenseDistribution['data']) !!},
|
||||
backgroundColor: ['#a855f7', '#f97316', '#64748b'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sales by Unit Chart
|
||||
const ctxSalesUnit = document.getElementById('salesUnitChart');
|
||||
if (ctxSalesUnit) {
|
||||
chartInstances.salesUnit = new Chart(ctxSalesUnit, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {!! json_encode($salesByUnit['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($salesByUnit['data']) !!},
|
||||
backgroundColor: ['#3b82f6', '#06b6d4', '#8b5cf6', '#ec4899'],
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Debt Status Chart
|
||||
const ctxDebtStatus = document.getElementById('debtStatusChart');
|
||||
if (ctxDebtStatus) {
|
||||
chartInstances.debtStatus = new Chart(ctxDebtStatus, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Sudah Bayar', 'Belum Bayar'],
|
||||
datasets: [{
|
||||
data: [{!! $debtStatus['paid'] !!}, {!! $debtStatus['unpaid'] !!}],
|
||||
backgroundColor: ['#10b981', '#f43f5e'],
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Feed Stock Chart
|
||||
const ctxFeedStock = document.getElementById('feedStockChart');
|
||||
if (ctxFeedStock) {
|
||||
chartInstances.feedStock = new Chart(ctxFeedStock, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {!! json_encode($feedStocks['labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Stok Tersedia',
|
||||
data: {!! json_encode($feedStocks['data']) !!},
|
||||
backgroundColor: '#f59e0b',
|
||||
borderRadius: 6
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
indexAxis: 'y',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Profit Trend Chart
|
||||
const ctxProfitTrend = document.getElementById('profitTrendChart');
|
||||
if (ctxProfitTrend) {
|
||||
chartInstances.profitTrend = new Chart(ctxProfitTrend, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {!! json_encode($financialChart['labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Laba Bersih',
|
||||
data: {!! json_encode($financialChart['profit']) !!},
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: '#10b98122',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Warehouse Productivity Chart
|
||||
const ctxWarehouse = document.getElementById('warehouseChart');
|
||||
if (ctxWarehouse) {
|
||||
chartInstances.warehouse = new Chart(ctxWarehouse, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {!! json_encode($productionByWarehouse['labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Total Produksi',
|
||||
data: {!! json_encode($productionByWarehouse['data']) !!},
|
||||
backgroundColor: '#6366f1',
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Revenue Split Chart
|
||||
const ctxRevenueSplit = document.getElementById('revenueSplitChart');
|
||||
if (ctxRevenueSplit) {
|
||||
chartInstances.revenueSplit = new Chart(ctxRevenueSplit, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($revenueSplit['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($revenueSplit['data']) !!},
|
||||
backgroundColor: ['#6366f1', '#94a3b8'],
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initCharts();
|
||||
});
|
||||
|
||||
document.addEventListener('stats-updated', () => {
|
||||
// Give Livewire time to update the DOM if necessary
|
||||
setTimeout(() => {
|
||||
initCharts();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Also hook into Livewire re-renders as backup
|
||||
document.addEventListener('livewire:initialized', () => {
|
||||
Livewire.on('stats-updated', () => {
|
||||
setTimeout(() => {
|
||||
initCharts();
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap');
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user