Compare commits
10 Commits
5c068017b2
...
51614c8454
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51614c8454 | ||
|
|
b12e20f49a | ||
|
|
a9e10849fd | ||
|
|
2b7535000a | ||
|
|
9619912f2f | ||
|
|
1eba57cb6b | ||
|
|
e11accb8cc | ||
|
|
b424d119fc | ||
|
|
77c750db40 | ||
|
|
84909d9392 |
@ -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 BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
||||
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\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Analisys extends Page
|
||||
class Analisys extends Page implements HasForms
|
||||
{
|
||||
use HasPageShield, 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([
|
||||
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');
|
||||
})
|
||||
->native(false),
|
||||
|
||||
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()
|
||||
->columns(3),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartPie;
|
||||
|
||||
protected static ?string $navigationLabel = 'Analisis';
|
||||
@ -28,15 +112,24 @@ class Analisys extends Page
|
||||
public function getViewData(): array
|
||||
{
|
||||
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
|
||||
$now = now();
|
||||
|
||||
// 1. Count customers
|
||||
$countPelanggan = Customer::count();
|
||||
$fromDateInput = $this->data['from_date'] ?? now()->startOfMonth()->format('d F Y');
|
||||
$toDateInput = $this->data['to_date'] ?? now()->format('d F Y');
|
||||
|
||||
// 2. Count total eggs (by unit)
|
||||
// Normalize for DB queries
|
||||
$fromDate = Carbon::parse($fromDateInput)->format('Y-m-d');
|
||||
$toDate = Carbon::parse($toDateInput)->format('Y-m-d');
|
||||
|
||||
// 1. Customer Count
|
||||
$customerCount = Customer::count();
|
||||
|
||||
// 2. Egg Production by Unit - Optimized join query
|
||||
$totalEggsByUnit = EggCollectionItem::query()
|
||||
->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')
|
||||
@ -48,12 +141,17 @@ public function getViewData(): array
|
||||
'total_broken' => $formatNumber($item->total_broken),
|
||||
]);
|
||||
|
||||
// 3. Count total orders
|
||||
$countPesanan = Order::count();
|
||||
// 3. Orders Stats - Combined count and sum
|
||||
$orderStats = Order::whereBetween('order_date', [$fromDate, $toDate])
|
||||
->selectRaw('count(*) as count, sum(total_amount) as total')
|
||||
->first();
|
||||
$orderCount = (int) ($orderStats->count ?? 0);
|
||||
$orderRevenue = (float) ($orderStats->total ?? 0);
|
||||
|
||||
// 4. Count total delayed goods (by unit)
|
||||
$totalDelayedByUnit = DelayedGood::query()
|
||||
// 4. Delayed Goods Stats - Combined by unit
|
||||
$delayedStatsByUnit = 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,45 +160,156 @@ 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');
|
||||
$totalPendapatan = $pendapatanPesanan + $pendapatanTertunda;
|
||||
// 5. Financial Totals in Range
|
||||
$delayedTotals = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])
|
||||
->selectRaw('sum(total_amount) as total, sum(paid_amount) as paid')
|
||||
->first();
|
||||
|
||||
// 6. Expenses
|
||||
$totalPengeluaran = Expense::sum('amount');
|
||||
$delayedRevenue = (float) ($delayedTotals->total ?? 0);
|
||||
$paidAmountInRange = (float) ($delayedTotals->paid ?? 0);
|
||||
$totalRevenue = $orderRevenue + $delayedRevenue;
|
||||
|
||||
// 7. Payroll
|
||||
$totalPenggajian = Payroll::sum('total_salary');
|
||||
$totalExpenses = (float) Expense::whereBetween('expense_date', [$fromDate, $toDate])->sum('amount');
|
||||
$totalPayroll = (float) Payroll::whereBetween('period_month', [Carbon::parse($fromDate)->format('Y-m'), Carbon::parse($toDate)->format('Y-m')])->sum('total_salary');
|
||||
$totalFeedPurchase = (float) FeedPurchase::whereBetween('purchase_date', [$fromDate, $toDate])->sum('total_price');
|
||||
|
||||
// 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');
|
||||
$grossProfit = $totalRevenue - $totalFeedPurchase;
|
||||
$netProfit = $grossProfit - $totalExpenses - $totalPayroll;
|
||||
$totalReceivables = $delayedRevenue - $paidAmountInRange;
|
||||
|
||||
$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 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'],
|
||||
['label' => 'Total Penggajian', 'desc' => 'Total gaji karyawan', 'value' => $formatNumber($totalPenggajian), 'prefix' => 'Rp'],
|
||||
['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalBelanjaPakan), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Pelanggan', 'desc' => 'Jumlah pelanggan terdaftar', 'value' => $customerCount, 'prefix' => ''],
|
||||
['label' => 'Total Pesanan', 'desc' => 'Jumlah transaksi pesanan', 'value' => $orderCount, 'prefix' => ''],
|
||||
['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi beban operasional', 'value' => $formatNumber($netProfit), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Piutang', 'desc' => 'Piutang barang tertunda (periode)', 'value' => $formatNumber($totalReceivables), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Pendapatan', 'desc' => 'Akumulasi semua pemasukan', 'value' => $formatNumber($totalRevenue), 'prefix' => 'Rp'],
|
||||
['label' => 'Laba Kotor', 'desc' => 'Pendapatan dikurangi biaya pakan', 'value' => $formatNumber($grossProfit), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Pengeluaran', 'desc' => 'Biaya operasional & umum', 'value' => $formatNumber($totalExpenses), 'prefix' => 'Rp'],
|
||||
['label' => 'Total Penggajian', 'desc' => 'Total gaji karyawan', 'value' => $formatNumber($totalPayroll), 'prefix' => 'Rp'],
|
||||
['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalFeedPurchase), 'prefix' => 'Rp'],
|
||||
];
|
||||
|
||||
// 6. Top 5 Customers - Optimized summing
|
||||
$topCustomers = Customer::query()
|
||||
->select('id', 'name', 'phone_number')
|
||||
->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 (Optimized query)
|
||||
$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();
|
||||
|
||||
$productionDataMap = EggCollection::whereBetween('production_date', [$fromDate, $toDate])
|
||||
->selectRaw('production_date, sum(total_eggs) as good, sum(total_broken_eggs) as broken')
|
||||
->groupBy('production_date')
|
||||
->get()
|
||||
->keyBy('production_date');
|
||||
|
||||
$productionChartData = [
|
||||
'labels' => $rangeQueryDays->map(fn ($d) => Carbon::parse($d)->format('d M'))->values(),
|
||||
'good' => $rangeQueryDays->map(fn ($d) => (float) ($productionDataMap[$d]->good ?? 0))->values(),
|
||||
'broken' => $rangeQueryDays->map(fn ($d) => (float) ($productionDataMap[$d]->broken ?? 0))->values(),
|
||||
];
|
||||
|
||||
// 8. Monthly Financial Trends (Optimized aggregates)
|
||||
$months = collect(range(0, 5))->map(fn ($i) => Carbon::parse($toDate)->subMonths($i)->format('Y-m'))->reverse();
|
||||
|
||||
$monthlyOrderRev = Order::whereIn(DB::raw("DATE_FORMAT(order_date, '%Y-%m')"), $months)
|
||||
->selectRaw("DATE_FORMAT(order_date, '%Y-%m') as month, sum(total_amount) as total")
|
||||
->groupBy('month')->pluck('total', 'month');
|
||||
|
||||
$monthlyDelayedRev = DelayedGood::whereIn(DB::raw("DATE_FORMAT(stored_date, '%Y-%m')"), $months)
|
||||
->selectRaw("DATE_FORMAT(stored_date, '%Y-%m') as month, sum(total_amount) as total")
|
||||
->groupBy('month')->pluck('total', 'month');
|
||||
|
||||
$monthlyExpenses = Expense::whereIn(DB::raw("DATE_FORMAT(expense_date, '%Y-%m')"), $months)
|
||||
->selectRaw("DATE_FORMAT(expense_date, '%Y-%m') as month, sum(amount) as total")
|
||||
->groupBy('month')->pluck('total', 'month');
|
||||
|
||||
$monthlyPayroll = Payroll::whereIn('period_month', $months)
|
||||
->selectRaw('period_month as month, sum(total_salary) as total')
|
||||
->groupBy('month')->pluck('total', 'month');
|
||||
|
||||
$monthlyFeed = FeedPurchase::whereIn(DB::raw("DATE_FORMAT(purchase_date, '%Y-%m')"), $months)
|
||||
->selectRaw("DATE_FORMAT(purchase_date, '%Y-%m') as month, sum(total_price) as total")
|
||||
->groupBy('month')->pluck('total', 'month');
|
||||
|
||||
$financialChartData = [
|
||||
'labels' => $months->map(fn ($m) => Carbon::parse($m)->translatedFormat('M Y'))->values(),
|
||||
'revenue' => $months->map(fn ($m) => (float) (($monthlyOrderRev[$m] ?? 0) + ($monthlyDelayedRev[$m] ?? 0)))->values(),
|
||||
'expenses' => $months->map(fn ($m) => (float) (($monthlyExpenses[$m] ?? 0) + ($monthlyPayroll[$m] ?? 0) + ($monthlyFeed[$m] ?? 0)))->values(),
|
||||
];
|
||||
$financialChartData['profit'] = $financialChartData['revenue']->map(fn ($rev, $i) => (float) ($rev - $financialChartData['expenses'][$i]))->values();
|
||||
|
||||
// 9. Distribution Charts
|
||||
$expenseDistributionData = [
|
||||
'labels' => ['Gaji Karyawan', 'Belanja Pakan', 'Pengeluaran Umum'],
|
||||
'data' => [(float) $totalPayroll, (float) $totalFeedPurchase, (float) $totalExpenses],
|
||||
];
|
||||
|
||||
$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();
|
||||
|
||||
$debtStatusData = [
|
||||
'paid' => (float) $paidAmountInRange,
|
||||
'unpaid' => (float) ($delayedRevenue - $paidAmountInRange),
|
||||
];
|
||||
|
||||
$feedStocks = Feed::query()
|
||||
->join('units', 'feeds.unit_id', '=', 'units.id')
|
||||
->select('feeds.name', 'feeds.stock', 'units.alias as unit_alias')
|
||||
->get();
|
||||
|
||||
$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();
|
||||
|
||||
$revenueSplitData = [
|
||||
'orders' => (float) $orderRevenue,
|
||||
'delayed' => (float) $delayedRevenue,
|
||||
];
|
||||
|
||||
return [
|
||||
'mainStats' => $mainStats,
|
||||
'totalEggsByUnit' => $totalEggsByUnit,
|
||||
'totalDelayedByUnit' => $totalDelayedByUnit,
|
||||
'totalDelayedByUnit' => $delayedStatsByUnit,
|
||||
'topCustomers' => $topCustomers,
|
||||
'productionChart' => $productionChartData,
|
||||
'financialChart' => $financialChartData,
|
||||
'expenseDistribution' => $expenseDistributionData,
|
||||
'salesByUnit' => [
|
||||
'labels' => $salesByUnit->pluck('name')->values(),
|
||||
'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(),
|
||||
],
|
||||
'debtStatus' => $debtStatusData,
|
||||
'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' => [$revenueSplitData['orders'], $revenueSplitData['delayed']],
|
||||
],
|
||||
'formatNumber' => $formatNumber,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
144
app/Filament/Pages/Dashboard.php
Normal file
144
app/Filament/Pages/Dashboard.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Models\Customer;
|
||||
use App\Models\DelayedGood;
|
||||
use App\Models\EggCollectionItem;
|
||||
use App\Models\Expense;
|
||||
use App\Models\FeedPurchase;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payroll;
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
use Filament\Widgets\AccountWidget;
|
||||
|
||||
class Dashboard extends BaseDashboard
|
||||
{
|
||||
protected static ?string $title = 'Dasbor';
|
||||
|
||||
protected string $view = 'filament.pages.dashboard';
|
||||
|
||||
public function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
AccountWidget::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
|
||||
$now = now();
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
// 1. Egg Production by Unit - Single optimized query with joins
|
||||
$totalEggsByUnit = EggCollectionItem::query()
|
||||
->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')
|
||||
->where('egg_collections.production_date', $today)
|
||||
->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')
|
||||
->groupBy('units.name')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'unit_name' => $item->unit_name,
|
||||
'total_good' => $formatNumber($item->total_good),
|
||||
'total_broken' => $formatNumber($item->total_broken),
|
||||
]);
|
||||
|
||||
// 2. Orders Stats - Combined count and sum
|
||||
$orderStats = Order::whereDate('order_date', $today)
|
||||
->selectRaw('count(*) as count, sum(total_amount) as total')
|
||||
->first();
|
||||
$orderCount = (int) ($orderStats->count ?? 0);
|
||||
$orderRevenue = (float) ($orderStats->total ?? 0);
|
||||
|
||||
// 3. Delayed Goods Stats - Combined sum and paid_amount
|
||||
$delayedStatsByUnit = DelayedGood::query()
|
||||
->join('units', 'delayed_goods.unit_id', 'units.id')
|
||||
->whereDate('delayed_goods.stored_date', $today)
|
||||
->selectRaw('units.name as unit_name, sum(quantity) as total')
|
||||
->groupBy('units.name')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'unit_name' => $item->unit_name,
|
||||
'total' => $formatNumber($item->total),
|
||||
]);
|
||||
|
||||
$delayedStatsTotals = DelayedGood::whereDate('stored_date', $today)
|
||||
->selectRaw('sum(total_amount) as total, sum(paid_amount) as paid')
|
||||
->first();
|
||||
|
||||
$delayedRevenue = (float) ($delayedStatsTotals->total ?? 0);
|
||||
$paidToday = (float) ($delayedStatsTotals->paid ?? 0);
|
||||
$totalRevenue = $orderRevenue + $delayedRevenue;
|
||||
|
||||
// 4. Expenses & Financials Today
|
||||
$totalExpenses = (float) Expense::whereDate('expense_date', $today)->sum('amount');
|
||||
$monthlyPayroll = (float) Payroll::where('period_month', $now->format('Y-m'))->sum('total_salary');
|
||||
$totalFeedPurchase = (float) FeedPurchase::whereDate('purchase_date', $today)->sum('total_price');
|
||||
|
||||
$grossProfit = $totalRevenue - $totalFeedPurchase;
|
||||
$netProfit = $grossProfit - $totalExpenses - $monthlyPayroll;
|
||||
$totalReceivablesToday = $delayedRevenue - $paidToday;
|
||||
|
||||
$mainStats = [
|
||||
['label' => 'Pesanan', 'desc' => 'Jumlah transaksi', 'value' => $orderCount, 'prefix' => ''],
|
||||
['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi pengeluaran harian', 'value' => $formatNumber($netProfit), 'prefix' => 'Rp'],
|
||||
['label' => 'Piutang', 'desc' => 'Piutang barang tertunda', 'value' => $formatNumber($totalReceivablesToday), 'prefix' => 'Rp'],
|
||||
['label' => 'Pendapatan', 'desc' => 'Akumulasi pemasukan', 'value' => $formatNumber($totalRevenue), 'prefix' => 'Rp'],
|
||||
['label' => 'Belanja Pakan', 'desc' => 'Baru dibeli', 'value' => $formatNumber($totalFeedPurchase), 'prefix' => 'Rp'],
|
||||
];
|
||||
|
||||
// 5. Top Customers Today - Optimized summing
|
||||
$topCustomers = Customer::query()
|
||||
->select('id', 'name', 'phone_number')
|
||||
->withSum(['orders' => fn ($q) => $q->whereDate('order_date', $today)], 'total_amount')
|
||||
->withSum(['delayedGoods' => fn ($q) => $q->whereDate('stored_date', $today)], 'total_amount')
|
||||
->get()
|
||||
->map(function ($customer) {
|
||||
$customer->total_spent = ($customer->orders_sum_total_amount ?? 0) + ($customer->delayed_goods_sum_total_amount ?? 0);
|
||||
|
||||
return $customer;
|
||||
})
|
||||
->filter(fn ($c) => $c->total_spent > 0)
|
||||
->sortByDesc('total_spent')
|
||||
->take(5)
|
||||
->values();
|
||||
|
||||
// 6. Distribution Charts Data
|
||||
$revenueSplitData = [
|
||||
'labels' => ['Penjualan Langsung', 'Barang Tertunda'],
|
||||
'data' => [$orderRevenue, $delayedRevenue],
|
||||
];
|
||||
|
||||
$expenseDistributionData = [
|
||||
'labels' => ['Belanja Pakan', 'Pengeluaran Umum'],
|
||||
'data' => [$totalFeedPurchase, $totalExpenses],
|
||||
];
|
||||
|
||||
$salesByUnit = Order::whereDate('order_date', $today)
|
||||
->join('units', 'orders.unit_id', 'units.id')
|
||||
->selectRaw('units.name, sum(total_amount) as total')
|
||||
->groupBy('units.name')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'isAdministrator' => auth()->user()->hasRole(RoleEnum::ADMINISTRATOR->value),
|
||||
'mainStats' => $mainStats,
|
||||
'totalEggsByUnit' => $totalEggsByUnit,
|
||||
'totalDelayedByUnit' => $delayedStatsByUnit,
|
||||
'topCustomers' => $topCustomers,
|
||||
'revenueSplit' => $revenueSplitData,
|
||||
'expenseDistribution' => $expenseDistributionData,
|
||||
'salesByUnit' => [
|
||||
'labels' => $salesByUnit->pluck('name')->values(),
|
||||
'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(),
|
||||
],
|
||||
'formatNumber' => $formatNumber,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -89,8 +89,7 @@ public static function configure(Schema $schema): Schema
|
||||
->relationship('customer', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->native(false)
|
||||
->required(),
|
||||
->native(false),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
|
||||
@ -9,6 +9,14 @@ trait HasCart
|
||||
{
|
||||
public array $cart = [];
|
||||
|
||||
public bool $isQtyModalOpen = false;
|
||||
|
||||
public ?int $modalFeedId = null;
|
||||
|
||||
public ?int $modalQty = null;
|
||||
|
||||
public ?string $modalFeedName = null;
|
||||
|
||||
public function loadCartFromDb(): void
|
||||
{
|
||||
$id = isset($this->record) ? $this->record->id : null;
|
||||
@ -20,6 +28,35 @@ public function loadCartFromDb(): void
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function openQtyModal(int $feedId): void
|
||||
{
|
||||
$feed = Feed::find($feedId);
|
||||
if (! $feed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->modalFeedId = $feedId;
|
||||
$this->modalFeedName = $feed->name;
|
||||
$this->modalQty = $this->cart[$feedId]['qty'] ?? 0;
|
||||
$this->isQtyModalOpen = true;
|
||||
|
||||
$this->dispatch('open-modal', id: 'qty-modal');
|
||||
}
|
||||
|
||||
public function saveQtyModal($qty = null): void
|
||||
{
|
||||
if ($qty !== null) {
|
||||
$this->modalQty = $qty;
|
||||
}
|
||||
|
||||
if ($this->modalFeedId) {
|
||||
$this->updateQty($this->modalFeedId, (int) $this->modalQty);
|
||||
}
|
||||
$this->isQtyModalOpen = false;
|
||||
|
||||
$this->dispatch('close-modal', id: 'qty-modal');
|
||||
}
|
||||
|
||||
public function addFeed(int $feedId): void
|
||||
{
|
||||
$feed = Feed::find($feedId);
|
||||
@ -81,6 +118,60 @@ public function decreaseQty(int $feedId): void
|
||||
}
|
||||
}
|
||||
|
||||
public function updateQty(int $feedId, $qty): void
|
||||
{
|
||||
$qty = (int) $qty;
|
||||
$feed = Feed::find($feedId);
|
||||
if (! $feed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = isset($this->record) ? $this->record->id : null;
|
||||
|
||||
$item = FeedPurchaseItem::where('feed_purchase_id', $id)
|
||||
->where('feed_id', $feedId)
|
||||
->first();
|
||||
|
||||
if ($qty <= 0) {
|
||||
if ($item) {
|
||||
// Revert stock if editing
|
||||
if ($id) {
|
||||
$item->feed?->decrement('stock', $item->quantity);
|
||||
}
|
||||
$item->delete();
|
||||
}
|
||||
} else {
|
||||
if ($item) {
|
||||
$oldQty = $item->quantity;
|
||||
$item->update([
|
||||
'quantity' => $qty,
|
||||
'subtotal' => $qty * $item->unit_price,
|
||||
]);
|
||||
|
||||
// Update stock if editing
|
||||
if ($id) {
|
||||
$feed->increment('stock', $qty - $oldQty);
|
||||
}
|
||||
} else {
|
||||
FeedPurchaseItem::create([
|
||||
'feed_purchase_id' => $id,
|
||||
'feed_id' => $feedId,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $feed->price,
|
||||
'subtotal' => $qty * $feed->price,
|
||||
]);
|
||||
|
||||
// Update stock if editing
|
||||
if ($id) {
|
||||
$feed->increment('stock', $qty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->loadCartFromDb();
|
||||
$this->dispatch('cart-updated');
|
||||
}
|
||||
|
||||
public function removeCartItem(int $itemId): void
|
||||
{
|
||||
$id = isset($this->record) ? $this->record->id : null;
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\EggCollection;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class EggProductionChart extends ChartWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected ?string $heading = 'Trend Produksi Telur (30 Hari Terakhir)';
|
||||
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = EggCollection::where('production_date', '>=', now()->subDays(30))
|
||||
->orderBy('production_date')
|
||||
->get()
|
||||
->groupBy(fn ($item) => Carbon::parse($item->production_date)->translatedFormat('Y-m-d'))
|
||||
->map(fn ($items) => $items->sum('total_eggs'));
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Total Telur',
|
||||
'data' => $data->values()->toArray(),
|
||||
'fill' => 'start',
|
||||
'tension' => 0.4,
|
||||
],
|
||||
],
|
||||
'labels' => $data->keys()->map(fn ($date) => Carbon::parse($date)->translatedFormat('d M'))->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Order;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class OrderTrendChart extends ChartWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected ?string $heading = 'Trend Jumlah Pesanan (30 Hari Terakhir)';
|
||||
|
||||
protected static ?int $sort = 4;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = Order::where('order_date', '>=', now()->subDays(30))
|
||||
->orderBy('order_date')
|
||||
->get()
|
||||
->groupBy(fn ($item) => Carbon::parse($item->order_date)->translatedFormat('Y-m-d'))
|
||||
->map(fn ($items) => $items->count());
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Jumlah Pesanan',
|
||||
'data' => $data->values()->toArray(),
|
||||
'fill' => 'start',
|
||||
'tension' => 0.4,
|
||||
],
|
||||
],
|
||||
'labels' => $data->keys()->map(fn ($date) => Carbon::parse($date)->translatedFormat('d M'))->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Models\FeedPurchase;
|
||||
use App\Models\Order;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RevenueExpenseChart extends ChartWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected ?string $heading = 'Pendapatan vs Pengeluaran (6 Bulan Terakhir)';
|
||||
|
||||
protected static ?int $sort = 3;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$months = collect(range(5, 0))->reverse()->map(function ($i) {
|
||||
return now()->subMonths($i)->format('Y-m');
|
||||
});
|
||||
|
||||
$revenues = Order::select(
|
||||
DB::raw("DATE_FORMAT(order_date, '%Y-%m') as month"),
|
||||
DB::raw('SUM(total_amount) as total')
|
||||
)
|
||||
->where('order_date', '>=', now()->subMonths(5)->startOfMonth())
|
||||
->groupBy('month')
|
||||
->get()
|
||||
->pluck('total', 'month');
|
||||
|
||||
$expenses = Expense::select(
|
||||
DB::raw("DATE_FORMAT(expense_date, '%Y-%m') as month"),
|
||||
DB::raw('SUM(amount) as total')
|
||||
)
|
||||
->where('expense_date', '>=', now()->subMonths(5)->startOfMonth())
|
||||
->groupBy('month')
|
||||
->get()
|
||||
->pluck('total', 'month');
|
||||
|
||||
$feedPurchases = FeedPurchase::select(
|
||||
DB::raw("DATE_FORMAT(purchase_date, '%Y-%m') as month"),
|
||||
DB::raw('SUM(total_price) as total')
|
||||
)
|
||||
->where('purchase_date', '>=', now()->subMonths(5)->startOfMonth())
|
||||
->groupBy('month')
|
||||
->get()
|
||||
->pluck('total', 'month');
|
||||
|
||||
$revenueData = $months->map(fn ($month) => (int) $revenues->get($month, 0));
|
||||
$expenseData = $months->map(fn ($month) => (int) ($expenses->get($month, 0) + $feedPurchases->get($month, 0)));
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Pendapatan (Order)',
|
||||
'data' => $revenueData->values()->toArray(),
|
||||
'backgroundColor' => '#10b981',
|
||||
'borderColor' => '#10b981',
|
||||
],
|
||||
[
|
||||
'label' => 'Pengeluaran (Operasional + Pakan)',
|
||||
'data' => $expenseData->values()->toArray(),
|
||||
'backgroundColor' => '#ef4444',
|
||||
'borderColor' => '#ef4444',
|
||||
],
|
||||
],
|
||||
'labels' => $months->values()->map(fn ($month) => Carbon::parse($month)->translatedFormat('M Y'))->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DelayedGood;
|
||||
use App\Models\EggCollection;
|
||||
use App\Models\Expense;
|
||||
use App\Models\FeedPurchase;
|
||||
use App\Models\Order;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class StatsOverview extends BaseWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
Stat::make('Produksi Telur', number_format(EggCollection::sum('total_eggs'), 0, ',', '.'))
|
||||
->description('Total akumulasi produksi telur (berbagai satuan).')
|
||||
->descriptionIcon('heroicon-m-circle-stack')
|
||||
->color('warning'),
|
||||
|
||||
Stat::make('Order', number_format(Order::count(), 0, ',', '.').' Pesanan')
|
||||
->description('Jumlah pesanan yang dibuat.')
|
||||
->descriptionIcon('heroicon-m-shopping-cart')
|
||||
->color('info'),
|
||||
|
||||
Stat::make('Pendapatan', 'Rp '.number_format(Order::sum('total_amount'), 0, ',', '.'))
|
||||
->description('Total pendapatan dari pesanan.')
|
||||
->descriptionIcon('heroicon-m-banknotes')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('Total Pelanggan', number_format(Customer::count(), 0, ',', '.').' Orang')
|
||||
->description('Total pelanggan terdaftar.')
|
||||
->descriptionIcon('heroicon-m-users')
|
||||
->color('primary'),
|
||||
|
||||
Stat::make('Total Pengeluaran', 'Rp '.number_format(Expense::sum('amount'), 0, ',', '.'))
|
||||
->description('Total akumulasi biaya pengeluaran.')
|
||||
->descriptionIcon('heroicon-m-credit-card')
|
||||
->color('danger'),
|
||||
|
||||
Stat::make('Barang Tertunda', number_format(DelayedGood::count(), 0, ',', '.').' Item')
|
||||
->description('Jumlah barang yang masih tertunda.')
|
||||
->descriptionIcon('heroicon-m-clock')
|
||||
->color('warning'),
|
||||
|
||||
Stat::make('Total Belanja Pakan', 'Rp '.number_format(FeedPurchase::sum('total_price'), 0, ',', '.'))
|
||||
->description('Total pengeluaran untuk pakan.')
|
||||
->descriptionIcon('heroicon-m-shopping-bag')
|
||||
->color('info'),
|
||||
|
||||
Stat::make('Laba Kotor', 'Rp '.number_format(Order::sum('total_amount') - (Expense::sum('amount') + FeedPurchase::sum('total_price')), 0, ',', '.'))
|
||||
->description('Pendapatan - (Pengeluaran + Pakan).')
|
||||
->descriptionIcon('heroicon-m-presentation-chart-line')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('Piutang Belum Terbayar', 'Rp '.number_format(DelayedGood::where('is_paid', false)->sum('total_amount') - DelayedGood::where('is_paid', false)->sum('paid_amount'), 0, ',', '.'))
|
||||
->description('Total piutang dari barang tertunda.')
|
||||
->descriptionIcon('heroicon-m-receipt-percent')
|
||||
->color('danger'),
|
||||
|
||||
Stat::make('Total Biaya Operasional', 'Rp '.number_format(Expense::sum('amount') + FeedPurchase::sum('total_price'), 0, ',', '.'))
|
||||
->description('Total beban biaya saat ini.')
|
||||
->descriptionIcon('heroicon-m-calculator')
|
||||
->color('warning'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Order;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TopCustomersChart extends ChartWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected ?string $heading = 'Top 5 Pelanggan (Berdasarkan Total Order)';
|
||||
|
||||
protected static ?int $sort = 6;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = Order::with('customer')
|
||||
->select('customer_id', DB::raw('SUM(total_amount) as total_spent'))
|
||||
->groupBy('customer_id')
|
||||
->orderByDesc('total_spent')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Total Pembelian (Rp)',
|
||||
'data' => $data->pluck('total_spent')->toArray(),
|
||||
'backgroundColor' => [
|
||||
'#fbbf24',
|
||||
'#f59e0b',
|
||||
'#d97706',
|
||||
'#b45309',
|
||||
'#92400e',
|
||||
],
|
||||
],
|
||||
],
|
||||
'labels' => $data->map(fn ($item) => $item->customer?->name ?? 'Umum')->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'bar';
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'indexAxis' => 'y',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use AchyutN\FilamentLogViewer\FilamentLogViewer;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Profile;
|
||||
use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
|
||||
use DiogoGPinto\AuthUIEnhancer\AuthUIEnhancerPlugin;
|
||||
@ -12,7 +13,6 @@
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Pages\Dashboard;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
@ -133,6 +133,7 @@ public function panel(Panel $panel): Panel
|
||||
->breadcrumbs(false)
|
||||
->globalSearch(false)
|
||||
->defaultAvatarProvider(FacehashProvider::class)
|
||||
->maxContentWidth(Width::Full);
|
||||
->maxContentWidth(Width::Full)
|
||||
->spa();
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,16 +18,26 @@ class DelayedGoodFactory extends Factory
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$quantity = fake()->numberBetween(1, 100);
|
||||
$unitPrice = fake()->numberBetween(5000, 20000);
|
||||
$unit = Unit::whereIn('alias', ['butir', 'kg'])->inRandomOrder()->first()
|
||||
?? Unit::where('alias', 'butir')->first()
|
||||
?? Unit::factory()->create();
|
||||
|
||||
if ($unit->alias === 'butir') {
|
||||
$quantity = fake()->numberBetween(10, 100);
|
||||
$unitPrice = fake()->numberBetween(1500, 2500);
|
||||
} else {
|
||||
$quantity = fake()->randomFloat(2, 1, 15);
|
||||
$unitPrice = fake()->numberBetween(25000, 32000);
|
||||
}
|
||||
|
||||
$totalAmount = $quantity * $unitPrice;
|
||||
$isPaid = fake()->boolean();
|
||||
$paidAmount = $isPaid ? $totalAmount : fake()->numberBetween(0, $totalAmount);
|
||||
$storedDate = fake()->dateTimeBetween('-1 month', 'now');
|
||||
|
||||
return [
|
||||
'customer_id' => Customer::factory(),
|
||||
'unit_id' => Unit::factory(),
|
||||
'customer_id' => Customer::inRandomOrder()->first()?->id ?? Customer::factory(),
|
||||
'unit_id' => $unit->id,
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'total_amount' => $totalAmount,
|
||||
|
||||
@ -18,8 +18,18 @@ public function definition(): array
|
||||
{
|
||||
return [
|
||||
'expense_date' => fake()->dateTimeBetween('-1 month', 'now'),
|
||||
'amount' => fake()->numberBetween(10000, 5000000),
|
||||
'notes' => fake()->sentence(),
|
||||
'amount' => fake()->numberBetween(50000, 2000000),
|
||||
'notes' => fake()->randomElement([
|
||||
'Listrik Kandang',
|
||||
'Gaji Pegawai',
|
||||
'Beli Vaksin/Obat',
|
||||
'Beli Vitamin',
|
||||
'Perbaikan Kandang',
|
||||
'Bahan Bakar Genset',
|
||||
'Operasional Kantor',
|
||||
'Sewa Kendaraan',
|
||||
'Beli ATK',
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\FeedDelivery;
|
||||
use App\Models\FeedSchedule;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FeedDeliveryFactory extends Factory
|
||||
@ -14,8 +13,7 @@ class FeedDeliveryFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'feed_schedule_id' => FeedSchedule::factory(),
|
||||
'feed_id' => Feed::factory(),
|
||||
'feed_id' => Feed::inRandomOrder()->first()?->id ?? Feed::factory(),
|
||||
'quantity' => $this->faker->numberBetween(1, 50),
|
||||
'delivered_at' => $this->faker->dateTimeBetween('-1 week', 'now'),
|
||||
];
|
||||
|
||||
@ -17,11 +17,13 @@ public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->randomElement([
|
||||
'Pakan Starter',
|
||||
'Pakan Grower',
|
||||
'Pakan Layer',
|
||||
'Konsentrat Ayam',
|
||||
'Pelet',
|
||||
'Jagung Giling',
|
||||
'Jagung Utuh',
|
||||
'Konsentrat Layer',
|
||||
'Bekatul/Dedak',
|
||||
'Mineral Mix',
|
||||
'Pakan Finisher',
|
||||
]),
|
||||
'unit_id' => Unit::inRandomOrder()->value('id') ?? Unit::factory(),
|
||||
'stock' => $this->faker->numberBetween(0, 500),
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\FeedSchedule;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
@ -13,10 +12,7 @@ class FeedScheduleFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'feed_id' => Feed::factory(),
|
||||
'quantity' => $this->faker->numberBetween(1, 50),
|
||||
'scheduled_at' => $this->faker->dateTimeBetween('-1 week', '+1 week'),
|
||||
'executed_at' => null,
|
||||
'scheduled_at' => $this->faker->time('H:i'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use App\Models\Unit;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
@ -23,19 +24,28 @@ public function definition(): array
|
||||
{
|
||||
$date = $this->faker->dateTimeBetween('-3 months', 'now');
|
||||
|
||||
$eggPrice = $this->faker->numberBetween(28000, 35000);
|
||||
$unit = Unit::whereIn('alias', ['butir', 'kg'])->inRandomOrder()->first()
|
||||
?? Unit::where('alias', 'butir')->first()
|
||||
?? Unit::factory()->create();
|
||||
|
||||
$customer = Customer::inRandomOrder()->first() ?? Customer::factory()->create();
|
||||
|
||||
$eggQuantity = $this->faker->randomFloat(2, 10, 500);
|
||||
$unitPrice = $eggPrice / 10;
|
||||
$discount = $this->faker->optional(0.3)->numberBetween(5000, 50000) ?? 0;
|
||||
if ($unit->alias === 'butir') {
|
||||
$eggQuantity = $this->faker->numberBetween(30, 300);
|
||||
$unitPrice = $this->faker->numberBetween(1500, 2500); // Price per egg
|
||||
} else {
|
||||
$eggQuantity = $this->faker->randomFloat(2, 5, 50);
|
||||
$unitPrice = $this->faker->numberBetween(25000, 32000); // Price per kg
|
||||
}
|
||||
|
||||
$discount = $this->faker->optional(0.3)->numberBetween(5000, 25000) ?? 0;
|
||||
$subtotal = $eggQuantity * $unitPrice;
|
||||
$totalAmount = max(0, $subtotal - $discount);
|
||||
|
||||
return [
|
||||
'customer_id' => $customer->id,
|
||||
'unit_id' => Unit::inRandomOrder()->first()->id ?? Unit::factory()->create()->id,
|
||||
'unit_id' => $unit->id,
|
||||
'warehouse_id' => Warehouse::inRandomOrder()->first()?->id ?? Warehouse::factory(),
|
||||
'egg_quantity' => $eggQuantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'order_date' => $date,
|
||||
|
||||
@ -21,6 +21,8 @@ public function run(): void
|
||||
UnitSeeder::class,
|
||||
WarehouseSeeder::class,
|
||||
FeedSeeder::class,
|
||||
FeedScheduleSeeder::class,
|
||||
FeedDeliverySeeder::class,
|
||||
FeedPurchaseSeeder::class,
|
||||
EggCollectionSeeder::class,
|
||||
CustomerSeeder::class,
|
||||
|
||||
17
database/seeders/FeedDeliverySeeder.php
Normal file
17
database/seeders/FeedDeliverySeeder.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\FeedDelivery;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class FeedDeliverySeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
FeedDelivery::factory()->count(30)->create();
|
||||
}
|
||||
}
|
||||
42
database/seeders/FeedScheduleSeeder.php
Normal file
42
database/seeders/FeedScheduleSeeder.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\FeedSchedule;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class FeedScheduleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$times = ['07:00', '12:00', '16:00'];
|
||||
$feeds = Feed::all();
|
||||
|
||||
if ($feeds->isEmpty()) {
|
||||
$this->call(FeedSeeder::class);
|
||||
$feeds = Feed::all();
|
||||
}
|
||||
|
||||
foreach ($times as $time) {
|
||||
$schedule = FeedSchedule::firstOrCreate([
|
||||
'scheduled_at' => $time,
|
||||
]);
|
||||
|
||||
// Attach 1-2 random feeds to each schedule
|
||||
$selectedFeeds = $feeds->random(rand(1, 2));
|
||||
|
||||
foreach ($selectedFeeds as $feed) {
|
||||
// Check if already attached to avoid duplicates if re-run
|
||||
if (! $schedule->feeds()->where('feed_id', $feed->id)->exists()) {
|
||||
$schedule->feeds()->attach($feed->id, [
|
||||
'quantity' => rand(10, 50),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\Unit;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class FeedSeeder extends Seeder
|
||||
@ -12,6 +13,25 @@ class FeedSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Feed::factory()->count(20)->create();
|
||||
$unitId = Unit::where('alias', 'kg')->first()?->id;
|
||||
|
||||
$feeds = [
|
||||
['name' => 'Pelet', 'price' => 7500],
|
||||
['name' => 'Jagung', 'price' => 6000],
|
||||
['name' => 'Konsentrat', 'price' => 12000],
|
||||
['name' => 'Bekatul', 'price' => 3500],
|
||||
['name' => 'Mineral Mix', 'price' => 15000],
|
||||
];
|
||||
|
||||
foreach ($feeds as $feed) {
|
||||
Feed::firstOrCreate(
|
||||
['name' => $feed['name']],
|
||||
[
|
||||
'unit_id' => $unitId,
|
||||
'price' => $feed['price'],
|
||||
'stock' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,11 +22,7 @@ public function run(): void
|
||||
"name": "Developer",
|
||||
"guard_name": "web",
|
||||
"permissions" : [
|
||||
"View:StatsOverview",
|
||||
"View:EggProductionChart",
|
||||
"View:OrderTrendChart",
|
||||
"View:RevenueExpenseChart",
|
||||
"View:TopCustomersChart",
|
||||
"View:Analisys",
|
||||
|
||||
"ViewAny:Customer",
|
||||
"View:Customer",
|
||||
@ -189,11 +185,7 @@ public function run(): void
|
||||
"name": "Pemilik",
|
||||
"guard_name": "web",
|
||||
"permissions" : [
|
||||
"View:StatsOverview",
|
||||
"View:EggProductionChart",
|
||||
"View:OrderTrendChart",
|
||||
"View:RevenueExpenseChart",
|
||||
"View:TopCustomersChart",
|
||||
"View:Analisys",
|
||||
|
||||
"ViewAny:Customer",
|
||||
"View:Customer",
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<form wire:submit="getViewData">
|
||||
{{ $this->form }}
|
||||
</form>
|
||||
|
||||
<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 +15,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-900 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_number ?? '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>
|
||||
<span
|
||||
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="text-right">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase block">Bagus</span>
|
||||
@ -51,7 +195,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 +215,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');
|
||||
|
||||
|
||||
248
resources/views/filament/pages/dashboard.blade.php
Normal file
248
resources/views/filament/pages/dashboard.blade.php
Normal file
@ -0,0 +1,248 @@
|
||||
<x-filament-panels::page>
|
||||
@if (!$isAdministrator)
|
||||
|
||||
<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-6 gap-4">
|
||||
@foreach ($mainStats as $stat)
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-4 transition hover:shadow-md border-b-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider">{{ $stat['label'] }}</span>
|
||||
<div class="flex items-baseline gap-1">
|
||||
@if ($stat['prefix'])
|
||||
<span
|
||||
class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span>
|
||||
@endif
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ $stat['value'] }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="text-[9px] text-gray-900 dark:text-gray-600 uppercase">{{ $stat['desc'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Distribution Row --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-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 Pemasukan
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="revenueSplitChart"></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
|
||||
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 per Satuan
|
||||
</h3>
|
||||
<div class="h-64 flex flex-col items-center">
|
||||
<canvas id="salesUnitChart"></canvas>
|
||||
</div>
|
||||
</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 flex items-center justify-between">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Produksi (Unit)</h3>
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span>
|
||||
</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">
|
||||
<span
|
||||
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="text-right">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase block">Bagus</span>
|
||||
<span
|
||||
class="text-sm font-bold text-emerald-600 dark:text-emerald-400">{{ $item['total_good'] }}</span>
|
||||
</div>
|
||||
<div class="text-right border-l border-gray-100 dark:border-gray-800 pl-4">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase block">Pecah</span>
|
||||
<span
|
||||
class="text-sm font-bold text-rose-600 dark:text-rose-400">{{ $item['total_broken'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada data produksi
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Delayed --}}
|
||||
<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 flex items-center justify-between">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Tertunda (Unit)</h3>
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@forelse($totalDelayedByUnit as $item)
|
||||
<div
|
||||
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
|
||||
<span
|
||||
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
|
||||
<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 italic">Tidak ada barang tertunda
|
||||
hari
|
||||
ini</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{{-- Top Customers Today --}}
|
||||
<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 Pelanggan Hari Ini</h3>
|
||||
</div>
|
||||
<div class="flex-1 divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@forelse ($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_number ?? 'Telp tidak ada' }}</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span
|
||||
class="text-[10px] font-semibold text-gray-400 uppercase block leading-none mb-1">Transaksi</span>
|
||||
<span class="text-sm font-black text-primary-600">Rp
|
||||
{{ $formatNumber($customer->total_spent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada transaksi hari
|
||||
ini
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!$isAdministrator)
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Revenue Split Chart
|
||||
const revenueSplitCtx = document.getElementById('revenueSplitChart');
|
||||
if (revenueSplitCtx) {
|
||||
new Chart(revenueSplitCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($revenueSplit['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($revenueSplit['data']) !!},
|
||||
backgroundColor: ['#6366f1', '#94a3b8'],
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expense Category Chart
|
||||
const expenseCtx = document.getElementById('expenseChart');
|
||||
if (expenseCtx) {
|
||||
new Chart(expenseCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($expenseDistribution['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($expenseDistribution['data']) !!},
|
||||
backgroundColor: ['#f59e0b', '#64748b'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sales by Unit Chart
|
||||
const salesUnitCtx = document.getElementById('salesUnitChart');
|
||||
if (salesUnitCtx) {
|
||||
new Chart(salesUnitCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {!! json_encode($salesByUnit['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($salesByUnit['data']) !!},
|
||||
backgroundColor: ['#3b82f6', '#06b6d4', '#8b5cf6', '#ec4899'],
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</x-filament-panels::page>
|
||||
@ -29,7 +29,10 @@ class="w-8 h-8 rounded-full border border-gray-200 dark:border-gray-600 flex ite
|
||||
-
|
||||
</a>
|
||||
|
||||
<span class="text-sm font-bold w-6 text-center text-gray-900 dark:text-gray-100">
|
||||
<span
|
||||
wire:click="openQtyModal({{ $feed->id }})"
|
||||
class="text-sm font-bold w-6 text-center text-gray-900 dark:text-gray-100 cursor-pointer hover:text-teal-500 transition-colors"
|
||||
>
|
||||
{{ $this->cart[$feed->id]['qty'] ?? 0 }}
|
||||
</span>
|
||||
|
||||
@ -42,6 +45,36 @@ class="w-8 h-8 rounded-full flex items-center justify-center text-white text-lg
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
|
||||
<x-filament::modal id="qty-modal" wire:model.live="isQtyModalOpen" width="sm">
|
||||
<x-slot name="heading">
|
||||
Atur Jumlah: {{ $this->modalFeedName }}
|
||||
</x-slot>
|
||||
|
||||
<div x-data="{ localQty: 0 }" x-on:open-modal.window="if ($event.detail.id === 'qty-modal') { localQty = $wire.modalQty }">
|
||||
<div class="py-2">
|
||||
<x-filament::input.wrapper>
|
||||
<x-filament::input
|
||||
type="number"
|
||||
x-model="localQty"
|
||||
x-on:keydown.enter="$wire.saveQtyModal(localQty)"
|
||||
placeholder="Masukan jumlah..."
|
||||
autofocus
|
||||
/>
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-4">
|
||||
<x-filament::button color="gray" wire:click="$set('isQtyModalOpen', false)" size="sm">
|
||||
Batal
|
||||
</x-filament::button>
|
||||
<x-filament::button x-on:click="$wire.saveQtyModal(localQty)" size="sm">
|
||||
Simpan
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::modal>
|
||||
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user