From 6627422655b807af0de67de92e3c0301bae762d2 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 8 Aug 2026 13:40:18 +0700 Subject: [PATCH] feat: enhance analysis page with additional revenue metrics and improve data handling --- app/Models/EmployeeAdvance.php | 2 +- app/Services/AnalysisService.php | 82 +++++++++++---------- resources/js/pages/admin/analysis/index.tsx | 44 +++++------ 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 82161b8..09b059e 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -70,7 +70,7 @@ protected function formattedRemainingAmount(): Attribute protected function statusLabel(): Attribute { return Attribute::make( - get: fn () => $this->status->label(), + get: fn () => $this->status?->label(), ); } diff --git a/app/Services/AnalysisService.php b/app/Services/AnalysisService.php index 5bbbb5b..fc9ede4 100644 --- a/app/Services/AnalysisService.php +++ b/app/Services/AnalysisService.php @@ -3,6 +3,8 @@ namespace App\Services; use App\Enums\OrderStatus; +use App\Enums\PriceType; +use App\Enums\RawMaterialUnit; use App\Models\Attendance; use App\Models\CashAccount; use App\Models\Employee; @@ -34,7 +36,7 @@ public function getAttendanceStats(?string $startDate, ?string $endDate): array $current->addDay(); } - $totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count(); + $totalEmployees = Employee::whereHas('user', fn($q) => $q->where('is_active', true))->count(); $present = Attendance::whereBetween('attendance_date', [$start, $end])->count(); @@ -128,24 +130,24 @@ public function getCashOverview(): array return [ 'total_balance' => $cashAccount->balance, 'total_transactions' => (clone $transactions)->count(), - 'total_deposit' => (clone $transactions)->where('type', 'deposit')->sum('amount'), - 'total_withdrawal' => (clone $transactions)->where('type', 'withdrawal')->sum('amount'), + 'total_deposit' => (int) (clone $transactions)->where('type', 'deposit')->sum('amount'), + 'total_withdrawal' => (int) (clone $transactions)->where('type', 'withdrawal')->sum('amount'), ]; } public function getRawMaterialStock(): array { - $prices = RawMaterialPrice::select('stock', 'price') + $prices = RawMaterialPrice::select('stock', 'price', 'raw_material_id') ->with('rawMaterial:id,unit') ->get(); $totalStock = $prices->sum('stock'); - $totalValue = $prices->sum(fn ($p) => $p->stock * $p->price); + $totalValue = $prices->sum(fn($p) => $p->stock * $p->price); $byUnit = [ - 'yard' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'yard')->sum('stock'), - 'meter' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'meter')->sum('stock'), - 'kilogram' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'kg')->sum('stock'), + 'yard' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'), + 'meter' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'), + 'kilogram' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'), ]; return [ @@ -157,8 +159,8 @@ public function getRawMaterialStock(): array public function getProductStock(): array { - $variants = ProductVariant::select('stock', 'reject_stock', 'retail_stock') - ->with('product:id,name') + $variants = ProductVariant::select('id', 'product_id', 'stock', 'reject_stock', 'retail_stock') + ->with(['product:id,name', 'productPrices' => fn ($q) => $q->where('type', PriceType::CAPITAL)]) ->get(); $totalStock = $variants->sum('stock'); @@ -166,9 +168,9 @@ public function getProductStock(): array $totalRetail = $variants->sum('retail_stock'); $totalValue = $variants->sum(function ($v) { - $retailPrice = $v->productPrices()->where('type', 'retail')->first()?->price ?? 0; + $capitalPrice = $v->productPrices->first()?->price ?? 0; - return $v->stock * $retailPrice; + return $v->stock * $capitalPrice; }); return [ @@ -191,15 +193,19 @@ public function getRevenueSummary(?string $startDate, ?string $endDate): array ->selectRaw('COUNT(*) as total_orders') ->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') - ->selectRaw('COALESCE(SUM(total_amount), 0) - COALESCE(SUM(total_amount), 0) as total_marketplace_fees') ->selectRaw('COALESCE(SUM(discount), 0) as total_deduction') + ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') + ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") + ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") ->first(); return [ 'total_revenue' => (int) $stats->total_revenue, 'total_discount' => (int) $stats->total_discount, - 'total_marketplace_fees' => (int) $stats->total_marketplace_fees, 'total_deduction' => (int) $stats->total_deduction, + 'net' => (int) $stats->net, + 'net_warehouse' => (int) $stats->net_warehouse, + 'net_retail' => (int) $stats->net_retail, 'total_orders' => (int) $stats->total_orders, 'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0, ]; @@ -214,14 +220,15 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate): array ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') - ->selectRaw('0 as net_warehouse') - ->selectRaw('0 as net_retail') + ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") + ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") ->selectRaw('COALESCE(SUM(discount), 0) as deduction') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) - ->get(); + ->get() + ->map(fn($item) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction'])); - return $monthly->toArray(); + return $monthly->values()->toArray(); } public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array @@ -236,9 +243,10 @@ public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate) ->selectRaw("SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END) as tiktok") ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) - ->get(); + ->get() + ->map(fn($item) => $item->only(['month', 'store', 'shopee', 'tiktok'])); - return $monthly->toArray(); + return $monthly->values()->toArray(); } public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array @@ -251,7 +259,7 @@ public function getRevenueByPaymentType(?string $startDate, ?string $endDate): a ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->groupBy('payment_type') ->get() - ->map(fn ($item) => [ + ->map(fn($item) => [ 'payment_type' => $item->payment_type, 'label' => $item->payment_type->label(), 'total' => (int) $item->total, @@ -288,7 +296,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array $expenseMonthly = Expense::query(); $this->applyDateFilter($expenseMonthly, $startDate, $endDate); - $expenseByMonth = (clone $expenseMonthly) + $expenseByMonth = (clone $expenseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as expense') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) @@ -298,7 +306,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array $purchaseMonthly = Purchase::query(); $this->applyDateFilter($purchaseMonthly, $startDate, $endDate); - $purchaseByMonth = (clone $purchaseMonthly) + $purchaseByMonth = (clone $purchaseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total), 0) as purchase') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) @@ -308,30 +316,30 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array $advanceMonthly = EmployeeAdvance::where('status', 'paid'); $this->applyDateFilter($advanceMonthly, $startDate, $endDate); - $advanceByMonth = (clone $advanceMonthly) + $advanceByMonth = (clone $advanceMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as advance') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); - $allMonths = collect(); + $allMonths = []; foreach ([$expenseByMonth, $purchaseByMonth, $advanceByMonth] as $data) { foreach ($data as $month => $row) { - if (! $allMonths->has($month)) { + if (! array_key_exists($month, $allMonths)) { $allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0]; } } } foreach ($allMonths as $month => &$row) { - $row['purchase'] = (int) ($purchaseByMonth[$month]['purchase'] ?? 0); - $row['expense'] = (int) ($expenseByMonth[$month]['expense'] ?? 0); - $row['advance'] = (int) ($advanceByMonth[$month]['advance'] ?? 0); + $row['purchase'] = (int) ($purchaseByMonth[$month]->purchase ?? 0); + $row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0); + $row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0); $row['total'] = $row['purchase'] + $row['expense'] + $row['advance']; } - return array_values($allMonths->toArray()); + return array_values($allMonths); } public function getBusyHours(?string $startDate, ?string $endDate): array @@ -393,7 +401,7 @@ public function getTopSuppliers(?string $startDate, ?string $endDate): array $query = Purchase::query(); $this->applyDateFilter($query, $startDate, $endDate); - return (clone $query) + return (clone $query)->toBase() ->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id') ->select('suppliers.name') ->selectRaw('COALESCE(SUM(purchases.total), 0) as total_amount') @@ -410,7 +418,7 @@ public function getTopCustomers(?string $startDate, ?string $endDate): array $query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id'); $this->applyDateFilter($query, $startDate, $endDate); - return (clone $query) + return (clone $query)->toBase() ->join('customers', 'orders.customer_id', '=', 'customers.id') ->select('customers.name') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_amount') @@ -427,7 +435,7 @@ public function getTopProducts(?string $startDate, ?string $endDate): array $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); - return (clone $query) + return (clone $query)->toBase() ->join('order_items', 'orders.id', '=', 'order_items.order_id') ->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id') ->join('products', 'product_variants.product_id', '=', 'products.id') @@ -447,7 +455,7 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array ->whereNotNull('orders.marketing_id'); $this->applyDateFilter($query, $startDate, $endDate); - return (clone $query) + return (clone $query)->toBase() ->join('users', 'orders.marketing_id', '=', 'users.id') ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') ->leftJoin('order_items', 'orders.id', '=', 'order_items.order_id') @@ -464,13 +472,13 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array ->toArray(); } - private function applyDateFilter($query, ?string $startDate, ?string $endDate): void + private function applyDateFilter($query, ?string $startDate, ?string $endDate, string $dateColumn = 'created_at'): void { if ($startDate) { - $query->whereDate('created_at', '>=', $startDate); + $query->whereDate($dateColumn, '>=', $startDate); } if ($endDate) { - $query->whereDate('created_at', '<=', $endDate); + $query->whereDate($dateColumn, '<=', $endDate); } } } diff --git a/resources/js/pages/admin/analysis/index.tsx b/resources/js/pages/admin/analysis/index.tsx index df4d048..83b3677 100644 --- a/resources/js/pages/admin/analysis/index.tsx +++ b/resources/js/pages/admin/analysis/index.tsx @@ -1,3 +1,17 @@ +import { StatCard } from '@/components/card/stat-card'; +import { DatePicker } from '@/components/inputs'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useCan } from '@/hooks/use-can'; +import { formatRupiah, formatRupiahShort } from '@/lib/rupiah'; import { Head, router } from '@inertiajs/react'; import { Banknote, @@ -19,20 +33,6 @@ import { XAxis, YAxis, } from 'recharts'; -import { StatCard } from '@/components/card/stat-card'; -import { DatePicker } from '@/components/inputs'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { useCan } from '@/hooks/use-can'; -import { formatRupiah, formatRupiahShort } from '@/lib/rupiah'; type Filters = { start_date?: string; @@ -83,8 +83,10 @@ type AnalysisProps = { revenueSummary: { total_revenue: number; total_discount: number; - total_marketplace_fees: number; total_deduction: number; + net: number; + net_warehouse: number; + net_retail: number; total_orders: number; avg_order: number; }; @@ -264,7 +266,6 @@ export default function Analysis({ marketingSales, }: AnalysisProps) { const { can, hasAnyRole, hasRole } = useCan(); - const [startDate, setStartDate] = useState(initialFilters.start_date ?? ''); const [endDate, setEndDate] = useState(initialFilters.end_date ?? ''); const [selectedPreset, setSelectedPreset] = useState(''); @@ -497,18 +498,18 @@ export default function Analysis({
{Object.entries(REVENUE_COLORS).filter(([key]) => { if (hasAnyRole(['owner', 'developer'])) { -return true; -} + return true; + } if (hasRole('cashier')) { -return key === 'total' || key === 'deduction'; -} + return key === 'total' || key === 'deduction'; + } return key === 'total' || key === 'net' || key === 'deduction'; }).map(([key]) => (
{key === 'net_warehouse' ? 'Total Gudang' : key === 'net_retail' ? 'Total Ecer' : key === 'total' ? 'Total' : key === 'net' ? 'Bersih' : 'Potongan'} - Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'deduction' ? revenueSummary.total_deduction : 0)} + Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'net' ? revenueSummary.net : key === 'net_warehouse' ? revenueSummary.net_warehouse : key === 'net_retail' ? revenueSummary.net_retail : revenueSummary.total_deduction)}
))}
@@ -641,6 +642,7 @@ return key === 'total' || key === 'deduction'; `Rp${formatRupiahShort(v)}`} /> } /> +