feat: enhance revenue analysis by adding payroll and expense calculations
This commit is contained in:
parent
78c432eaa0
commit
0aae00a96b
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user