feat: add payroll calculations to analysis and visualize revenue vs profit trends

This commit is contained in:
Yoga Pangestu 2026-04-23 19:58:02 +07:00
parent a280eb8a64
commit 51f2e65bd2
4 changed files with 151 additions and 4 deletions

View File

@ -8,6 +8,7 @@
use App\Models\Expense;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payroll;
use App\Models\Purchase;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
@ -20,6 +21,7 @@ public function __invoke()
'total_sales' => $this->getStats(fn () => Order::count()),
'total_revenue' => $this->getStats(fn () => Order::sum('total')),
'total_expenses' => $this->getStats(fn () => Expense::sum('amount')),
'total_payrolls' => $this->getStats(fn () => Payroll::sum('total_salary')),
'cogs' => $this->getStats(fn () => Order::sum('hpp')),
'total_discount' => $this->getStats(fn () => Order::sum('discount')),
'total_purchases' => $this->getStats(fn () => Purchase::sum('total')),
@ -29,7 +31,11 @@ public function __invoke()
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
$stats['gross_profit'] = $this->calculateDiff($stats['total_revenue'], $stats['cogs']);
$stats['net_profit'] = $this->calculateDiff($stats['gross_profit'], $stats['total_expenses']);
$stats['net_profit'] = [
'value' => $stats['gross_profit']['value'] - $stats['total_expenses']['value'] - $stats['total_payrolls']['value'],
'yesterday' => null,
'change' => null,
];
$stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
// Revenue vs Purchases per month (this year)
@ -131,9 +137,55 @@ public function __invoke()
];
});
// Revenue & Profit per month
$ordersByMonth = DB::table('orders')
->select(
DB::raw('MONTH(created_at) as month'),
DB::raw('SUM(total) as revenue'),
DB::raw('SUM(hpp) as cogs')
)
->groupBy(DB::raw('MONTH(created_at)'))
->get();
$expensesByMonth = DB::table('expenses')
->select(
DB::raw('MONTH(created_at) as month'),
DB::raw('SUM(amount) as total')
)
->groupBy(DB::raw('MONTH(created_at)'))
->get();
$payrollsByMonth = DB::table('payrolls')
->select(
DB::raw('MONTH(period_month) as month'),
DB::raw('SUM(total_salary) as total')
)
->whereNull('deleted_at')
->groupBy(DB::raw('MONTH(period_month)'))
->get();
$revenueVsProfit = collect(range(1, 12))->map(function ($month) use ($ordersByMonth, $expensesByMonth, $payrollsByMonth, $monthNames) {
$orderFound = $ordersByMonth->firstWhere('month', $month);
$expenseFound = $expensesByMonth->firstWhere('month', $month);
$payrollFound = $payrollsByMonth->firstWhere('month', $month);
$revenue = $orderFound ? (float) $orderFound->revenue : 0;
$cogs = $orderFound ? (float) $orderFound->cogs : 0;
$expense = $expenseFound ? (float) $expenseFound->total : 0;
$payroll = $payrollFound ? (float) $payrollFound->total : 0;
$profit = $revenue - $cogs - $expense - $payroll;
return [
'month' => $monthNames[$month - 1],
'revenue' => $revenue,
'profit' => $profit,
];
});
return Inertia::render('analysis', [
'stats' => $stats,
'salesByMonth' => $salesByMonth,
'revenueVsProfit' => $revenueVsProfit,
'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses,

View File

@ -0,0 +1,88 @@
"use client"
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import { formatCurrency } from "@/lib/formatters"
export const description = "Perbandingan Pendapatan vs Laba Bersih per bulan."
const chartConfig = {
revenue: {
label: "Pendapatan",
color: "var(--chart-1)",
},
profit: {
label: "Laba Bersih",
color: "var(--chart-2)",
},
} satisfies ChartConfig
export function RevenueVsProfitChart({ data }: { data: any[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Pendapatan vs Laba Bersih</CardTitle>
<CardDescription>Perbandingan Pendapatan vs Laba Bersih per bulan.</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="aspect-auto h-[300px] w-full">
<LineChart
accessibilityLayer
data={data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
formatter={(value, name) => [
formatCurrency(Number(value)),
` - ${chartConfig[name as keyof typeof chartConfig]?.label ?? name}`,
]}
/>
}
/>
<Line
dataKey="revenue"
type="monotone"
stroke="var(--color-revenue)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="profit"
type="monotone"
stroke="var(--color-profit)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@ -4,11 +4,12 @@ import type { AnalysisPageProps } from '@/types';
import { Head, usePage } from '@inertiajs/react';
import { CustomBarChart } from '../components/charts/bar-chart';
import { CustomPieChart } from '../components/charts/pie-chart';
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
export default function Analisa() {
const { stats, salesByMonth, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
const { stats, salesByMonth, revenueVsProfit, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
usePage<AnalysisPageProps>().props;
return (
@ -33,6 +34,7 @@ export default function Analisa() {
{ title: "Total Diskon", stat: stats.total_discount },
{ title: "Laba Kotor", stat: stats.gross_profit },
{ title: "Laba Bersih", stat: stats.net_profit },
{ title: "Total Gaji", stat: stats.total_payrolls },
{ title: "Total Pembelian", stat: stats.total_purchases },
{ title: "Produk Terjual", stat: stats.products_sold, isCurrency: false },
{ title: "Total Pelanggan", stat: stats.total_customers, isCurrency: false },
@ -51,6 +53,9 @@ export default function Analisa() {
</div>
<RevenueVsPurchasesChart data={salesByMonth} />
<RevenueVsProfitChart data={revenueVsProfit} />
<SalesByHourChart
data={salesByHour}
config={{

View File

@ -16,6 +16,7 @@ export interface DashboardStats {
total_discount: StatItem;
gross_profit: StatItem;
net_profit: StatItem;
total_payrolls: StatItem;
total_purchases: StatItem;
products_sold: StatItem;
total_customers: StatItem;
@ -31,7 +32,8 @@ export interface AnalysisPageProps {
auth: Auth;
stats: DashboardStats;
salesByMonth: any[];
ordersByHour: any[];
revenueVsProfit: any[];
salesByHour: any[];
paymentMethods: any[];
orderStatuses: any[];
orderChannels: any[];