feat: enhance revenue analysis by adding payroll and expense calculations

This commit is contained in:
Yoga Pangestu 2026-08-14 00:32:37 +07:00
parent 78c432eaa0
commit 0aae00a96b
5 changed files with 148 additions and 26 deletions

View File

@ -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,
];
}

View File

@ -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,
];
}

View File

@ -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);

View File

@ -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 (
<button
key={key}
@ -909,7 +921,7 @@ export default function Analysis({
<Card className="py-0" style={{ order: sectionOrder.revenueTrend ?? 99 }}>
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 sm:py-0!">
<CardTitle>Trend Pendapatan</CardTitle>
<CardTitle>Trend Penjualan</CardTitle>
</div>
<div className="flex flex-col sm:flex-row">
{revenueTrendKeys.map((key) => {

View File

@ -51,6 +51,10 @@ type DashboardProps = {
total_discount: number;
total_cogs: number;
total_deduction: number;
gross: number;
net: number;
payroll_total: number;
expense_total: number;
total_orders: number;
};
expenseSummary: {
@ -341,9 +345,21 @@ export default function Dashboard({
mainValue={`Rp${formatRupiah(revenueSummary.total_revenue)}`}
subLabel={`${revenueSummary.total_orders} transaksi selesai`}
items={[
{
label: 'Kotor',
value: `Rp${formatRupiah(revenueSummary.gross)}`,
},
{
label: 'Bersih',
value: `Rp${formatRupiah(revenueSummary.total_revenue - revenueSummary.total_cogs)}`,
value: `Rp${formatRupiah(revenueSummary.net)}`,
},
{
label: 'Gaji',
value: `Rp${formatRupiah(revenueSummary.payroll_total)}`,
},
{
label: 'Pengeluaran',
value: `Rp${formatRupiah(revenueSummary.expense_total)}`,
},
{
label: 'Potongan',