From 0aae00a96bb1812f47b2f62ec250df355402bbce Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 14 Aug 2026 00:32:37 +0700 Subject: [PATCH] feat: enhance revenue analysis by adding payroll and expense calculations --- app/Services/AnalysisService.php | 101 +++++++++++++++++--- app/Services/DashboardService.php | 23 ++++- resources/css/app.css | 4 + resources/js/pages/admin/analysis/index.tsx | 28 ++++-- resources/js/pages/dashboard.tsx | 18 +++- 5 files changed, 148 insertions(+), 26 deletions(-) diff --git a/app/Services/AnalysisService.php b/app/Services/AnalysisService.php index 39e9580..b14d291 100644 --- a/app/Services/AnalysisService.php +++ b/app/Services/AnalysisService.php @@ -7,6 +7,7 @@ use App\Enums\OrderChannel; use App\Enums\OrderStatus; use App\Enums\PaymentType; +use App\Enums\PayrollStatus; use App\Enums\RawMaterialUnit; use App\Enums\Role; use App\Models\Attendance; @@ -16,6 +17,7 @@ use App\Models\Expense; use App\Models\LeaveRequest; use App\Models\Order; +use App\Models\Payroll; use App\Models\Purchase; use App\Models\PurchaseItem; use App\Models\RestockItem; @@ -260,12 +262,28 @@ public function getRevenueSummary(?string $startDate, ?string $endDate, ?User $u ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') ->first(); + $payrollQuery = Payroll::where('status', PayrollStatus::PAID); + $this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at'); + $payrollTotal = (int) $payrollQuery->sum('total_amount'); + + $expenseQuery = Expense::query(); + $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); + $expenseTotal = (int) $expenseQuery->sum('amount'); + + $totalRevenue = (int) $stats->total_revenue; + $totalCogs = (int) $stats->total_cogs; + $gross = $totalRevenue - $totalCogs; + $net = $gross - $payrollTotal - $expenseTotal; + return [ - 'total_revenue' => (int) $stats->total_revenue, + 'total_revenue' => $totalRevenue, 'total_discount' => (int) $stats->total_discount, 'total_deduction' => (int) $stats->total_deduction, - 'cogs' => (int) $stats->total_cogs, - 'net' => (int) $stats->total_revenue - (int) $stats->total_cogs, + 'cogs' => $totalCogs, + 'gross' => $gross, + 'net' => $net, + 'payroll_total' => $payrollTotal, + 'expense_total' => $expenseTotal, 'total_orders' => (int) $stats->total_orders, 'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0, ]; @@ -283,23 +301,68 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate, ?User $u $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') - ->selectRaw('COALESCE(SUM(total_amount) - SUM(cogs), 0) as net') ->selectRaw('COALESCE(SUM(discount), 0) as discount') ->selectRaw('COALESCE(SUM(nego_price), 0) as deduction') ->selectRaw('COALESCE(SUM(cogs), 0) as cogs') ->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() - ->map(fn ($item) => [ - 'month' => $item->month, - 'total' => (int) $item->total, - 'net' => (int) $item->net, - 'discount' => (int) $item->discount, - 'deduction' => (int) $item->deduction, - 'cogs' => (int) $item->cogs, - ]); + ->keyBy('month'); - return $monthly->values()->toArray(); + $payrollMonthly = Payroll::where('status', PayrollStatus::PAID); + $this->applyDateFilter($payrollMonthly, $startDate, $endDate, 'paid_at'); + + $payrollByMonth = (clone $payrollMonthly)->toBase() + ->selectRaw("DATE_FORMAT(paid_at, '%b %Y') as month") + ->selectRaw('COALESCE(SUM(total_amount), 0) as payroll') + ->groupBy(DB::raw("DATE_FORMAT(paid_at, '%Y-%m')"), DB::raw("DATE_FORMAT(paid_at, '%b %Y')")) + ->get() + ->keyBy('month'); + + $expenseMonthly = Expense::query(); + $this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at'); + + $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')")) + ->get() + ->keyBy('month'); + + $allMonths = []; + foreach ([$monthly, $payrollByMonth, $expenseByMonth] as $data) { + foreach ($data as $month => $row) { + if (! array_key_exists($month, $allMonths)) { + $allMonths[$month] = $month; + } + } + } + + $result = []; + foreach ($allMonths as $month => $_) { + $total = (int) ($monthly[$month]->total ?? 0); + $discount = (int) ($monthly[$month]->discount ?? 0); + $deduction = (int) ($monthly[$month]->deduction ?? 0); + $cogs = (int) ($monthly[$month]->cogs ?? 0); + $payroll = (int) ($payrollByMonth[$month]->payroll ?? 0); + $expense = (int) ($expenseByMonth[$month]->expense ?? 0); + $gross = $total - $cogs; + $net = $gross - $payroll - $expense; + + $result[] = [ + 'month' => $month, + 'total' => $total, + 'gross' => $gross, + 'net' => $net, + 'discount' => $discount, + 'deduction' => $deduction, + 'cogs' => $cogs, + 'payroll' => $payroll, + 'expense' => $expense, + ]; + } + + return $result; } public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array @@ -541,8 +604,16 @@ public function getProfitMetrics(?string $startDate, ?string $endDate, ?User $us ->join('order_items', 'orders.id', '=', 'order_items.order_id') ->sum('order_items.quantity'); + $payrollQuery = Payroll::where('status', PayrollStatus::PAID); + $this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at'); + $payrollTotal = (int) $payrollQuery->sum('total_amount'); + + $expenseQuery = Expense::query(); + $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); + $expenseTotal = (int) $expenseQuery->sum('amount'); + $grossProfit = $stats->total_revenue - $stats->hpp; - $netProfit = $grossProfit; + $netProfit = $grossProfit - $payrollTotal - $expenseTotal; $profitMargin = $stats->total_revenue > 0 ? round(($netProfit / $stats->total_revenue) * 100, 1) : 0; $aov = $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0; $itemsPerTransaction = $stats->total_orders > 0 ? round($totalProductsSold / $stats->total_orders, 1) : 0; @@ -556,6 +627,8 @@ public function getProfitMetrics(?string $startDate, ?string $endDate, ?User $us 'profit_margin' => $profitMargin, 'aov' => $aov, 'items_per_transaction' => $itemsPerTransaction, + 'payroll_total' => $payrollTotal, + 'expense_total' => $expenseTotal, ]; } diff --git a/app/Services/DashboardService.php b/app/Services/DashboardService.php index c8da7e0..6f98658 100644 --- a/app/Services/DashboardService.php +++ b/app/Services/DashboardService.php @@ -5,6 +5,7 @@ use App\Enums\OrderChannel; use App\Enums\OrderStatus; use App\Enums\PaymentType; +use App\Enums\PayrollStatus; use App\Enums\Role; use App\Models\Attendance; use App\Models\Employee; @@ -12,6 +13,7 @@ use App\Models\Expense; use App\Models\LeaveRequest; use App\Models\Order; +use App\Models\Payroll; use App\Models\User; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; @@ -135,12 +137,27 @@ public function getRevenueSummary(?User $user = null): array ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') ->first(); + $payrollTotal = (int) Payroll::where('status', PayrollStatus::PAID) + ->whereDate('paid_at', $today) + ->sum('total_amount'); + + $expenseTotal = (int) Expense::whereDate('created_at', $today) + ->sum('amount'); + + $totalRevenue = (int) $stats->total_revenue; + $totalCogs = (int) $stats->total_cogs; + $gross = $totalRevenue - $totalCogs; + $net = $gross - $payrollTotal - $expenseTotal; + return [ - 'total_revenue' => (int) $stats->total_revenue, + 'total_revenue' => $totalRevenue, 'total_discount' => (int) $stats->total_discount, - 'total_cogs' => (int) $stats->total_cogs, + 'total_cogs' => $totalCogs, 'total_deduction' => (int) $stats->total_deduction, - 'net' => (int) $stats->total_revenue - (int) $stats->total_cogs, + 'gross' => $gross, + 'net' => $net, + 'payroll_total' => $payrollTotal, + 'expense_total' => $expenseTotal, 'total_orders' => (int) $stats->total_orders, ]; } diff --git a/resources/css/app.css b/resources/css/app.css index 95fd0fb..9f1b594 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -53,6 +53,7 @@ @theme { --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); + --color-chart-6: var(--chart-6); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); @@ -89,6 +90,7 @@ :root { --chart-3: oklch(0.666 0.179 58.318); --chart-4: oklch(0.555 0.163 48.998); --chart-5: oklch(0.473 0.137 46.201); + --chart-6: oklch(0.696 0.17 162.48); --radius: 0.625rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); @@ -125,6 +127,7 @@ .dark { --chart-3: oklch(0.666 0.179 58.318); --chart-4: oklch(0.555 0.163 48.998); --chart-5: oklch(0.473 0.137 46.201); + --chart-6: oklch(0.696 0.17 162.48); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.769 0.188 70.08); @@ -183,6 +186,7 @@ @theme inline { --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); --color-chart-5: var(--chart-5); + --color-chart-6: var(--chart-6); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); --color-chart-2: var(--chart-2); diff --git a/resources/js/pages/admin/analysis/index.tsx b/resources/js/pages/admin/analysis/index.tsx index a16c570..96e556e 100644 --- a/resources/js/pages/admin/analysis/index.tsx +++ b/resources/js/pages/admin/analysis/index.tsx @@ -86,17 +86,23 @@ type AnalysisProps = { total_discount: number; total_deduction: number; cogs: number; + gross: number; net: number; + payroll_total: number; + expense_total: number; total_orders: number; avg_order: number; }; monthlyRevenue: Array<{ month: string; total: number; + gross: number; net: number; discount: number; deduction: number; cogs: number; + payroll: number; + expense: number; }>; monthlyRevenueByChannel: Array<{ month: string; @@ -135,6 +141,8 @@ type AnalysisProps = { profit_margin: number; aov: number; items_per_transaction: number; + payroll_total: number; + expense_total: number; }; topSuppliers: Array<{ name: string; @@ -195,12 +203,16 @@ const revenueChartConfig = { label: 'Total', color: 'var(--chart-1)', }, - net: { - label: 'Bersih', + gross: { + label: 'Keuntungan Kotor', color: 'var(--chart-2)', }, + net: { + label: 'Keuntungan Bersih', + color: 'var(--chart-6)', + }, deduction: { - label: 'Potongan', + label: 'Potongan Nego', color: 'var(--chart-3)', }, discount: { @@ -213,7 +225,7 @@ const revenueChartConfig = { }, } satisfies ChartConfig; -const revenueKeys = ['total', 'net', 'deduction', 'discount', 'cogs'] as const; +const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const; const expenseChartConfig = { total: { @@ -225,7 +237,7 @@ const expenseChartConfig = { color: 'var(--chart-2)', }, expense: { - label: 'Pengeluaran', + label: 'Pengeluaran Toko', color: 'var(--chart-3)', }, advance: { @@ -638,9 +650,9 @@ export default function Analysis({ {revenueKeys.filter((key) => { if (hasAnyRole(['owner', 'developer'])) return true; if (hasRole('cashier')) return key === 'total' || key === 'discount' || key === 'deduction' || key === 'cogs'; - return key === 'total' || key === 'discount' || key === 'deduction' || key === 'net' || key === 'cogs'; + return key === 'total' || key === 'discount' || key === 'deduction' || key === 'cogs' || key === 'gross' || key === 'net'; }).map((key) => { - const value = key === 'total' ? revenueSummary.total_revenue : key === 'discount' ? revenueSummary.total_discount : key === 'net' ? revenueSummary.net : key === 'cogs' ? revenueSummary.cogs : revenueSummary.total_deduction; + const value = key === 'total' ? revenueSummary.total_revenue : key === 'discount' ? revenueSummary.total_discount : key === 'net' ? revenueSummary.net : key === 'gross' ? revenueSummary.gross : key === 'cogs' ? revenueSummary.cogs : revenueSummary.total_deduction; return (