feat: enhance monthly revenue calculations in AnalysisService to include stock quality breakdown; update Analysis.vue to display net revenue for gudang and ecer categories, improving data visibility for users
This commit is contained in:
parent
150a969086
commit
3a8a6982ec
@ -599,7 +599,7 @@ function () use ($cutting, $validated): void {
|
||||
foreach ($materials as $materialData) {
|
||||
$combinationId = $materialData['combination_id'];
|
||||
if ($combinationId !== null) {
|
||||
if (!isset($combinationGroups[$combinationId])) {
|
||||
if (! isset($combinationGroups[$combinationId])) {
|
||||
$combinationGroups[$combinationId] = [
|
||||
'materials' => [],
|
||||
'material_result' => $materialData['combination_material_result'] ?? null,
|
||||
@ -622,7 +622,7 @@ function () use ($cutting, $validated): void {
|
||||
$newCombinationId = $materialData['combination_id'] !== null
|
||||
? $combinationIdMap[$materialData['combination_id']] ?? null
|
||||
: null;
|
||||
|
||||
|
||||
$cutting->materials()->create([
|
||||
'raw_material_price_id' => $materialData['raw_material_price_id'],
|
||||
'material_usage' => $materialData['material_usage'],
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PriceType;
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
@ -285,7 +286,7 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate =
|
||||
->groupBy('month_key')
|
||||
->map(fn ($orders) => $orders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)));
|
||||
|
||||
$monthlyHpp = OrderItem::query()
|
||||
$monthlyItemsData = OrderItem::query()
|
||||
->join('orders', 'order_items.order_id', '=', 'orders.id')
|
||||
->leftJoin('product_prices', function ($join) {
|
||||
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
|
||||
@ -297,11 +298,13 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate =
|
||||
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
|
||||
->selectRaw("
|
||||
DATE_FORMAT(orders.created_at, '%Y-%m') as month_key,
|
||||
order_items.stock_quality,
|
||||
SUM(order_items.subtotal) as subtotal,
|
||||
COALESCE(SUM(order_items.quantity * product_prices.price), 0) as hpp
|
||||
")
|
||||
->groupBy('month_key')
|
||||
->groupBy('month_key', 'order_items.stock_quality')
|
||||
->get()
|
||||
->pluck('hpp', 'month_key');
|
||||
->groupBy('month_key');
|
||||
|
||||
if ($monthlyData->isEmpty()) {
|
||||
return [];
|
||||
@ -319,13 +322,42 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate =
|
||||
$revenue = $monthlyData->firstWhere('month_key', $key);
|
||||
$fees = $monthlyFees->get($key, 0);
|
||||
$discount = (int) ($revenue->total_discount ?? 0);
|
||||
$hpp = (int) ($monthlyHpp->get($key, 0));
|
||||
$deduction = $discount + $fees;
|
||||
|
||||
$monthItems = $monthlyItemsData->get($key, collect());
|
||||
$gudangItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good'
|
||||
);
|
||||
$ecerItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail'
|
||||
);
|
||||
$rejectItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'reject'
|
||||
);
|
||||
|
||||
$gudangSubtotal = (int) ($gudangItem?->subtotal ?? 0);
|
||||
$gudangHpp = (int) ($gudangItem?->hpp ?? 0);
|
||||
$ecerSubtotal = (int) ($ecerItem?->subtotal ?? 0);
|
||||
$ecerHpp = (int) ($ecerItem?->hpp ?? 0);
|
||||
$rejectSubtotal = (int) ($rejectItem?->subtotal ?? 0);
|
||||
$rejectHpp = (int) ($rejectItem?->hpp ?? 0);
|
||||
|
||||
$totalItemsSubtotal = $gudangSubtotal + $ecerSubtotal + $rejectSubtotal;
|
||||
$hpp = $gudangHpp + $ecerHpp + $rejectHpp;
|
||||
|
||||
$gudangDeduction = 0;
|
||||
$ecerDeduction = 0;
|
||||
if ($totalItemsSubtotal > 0) {
|
||||
$gudangDeduction = ($gudangSubtotal / $totalItemsSubtotal) * $deduction;
|
||||
$ecerDeduction = ($ecerSubtotal / $totalItemsSubtotal) * $deduction;
|
||||
}
|
||||
|
||||
$netGudang = $gudangSubtotal - $gudangDeduction - $gudangHpp;
|
||||
$netEcer = $ecerSubtotal - $ecerDeduction - $ecerHpp;
|
||||
|
||||
$result[] = [
|
||||
'month' => $monthLabel,
|
||||
'total' => (int) ($revenue->total_revenue ?? 0),
|
||||
'net' => (int) ($revenue->total_revenue ?? 0) - $deduction - $hpp,
|
||||
'net_gudang' => (int) round($netGudang),
|
||||
'net_ecer' => (int) round($netEcer),
|
||||
'deduction' => $deduction,
|
||||
];
|
||||
|
||||
|
||||
@ -97,6 +97,8 @@ const props = defineProps<{
|
||||
month: string;
|
||||
total: number;
|
||||
net: number;
|
||||
net_gudang: number;
|
||||
net_ecer: number;
|
||||
deduction: number;
|
||||
}>;
|
||||
expenseSummary: {
|
||||
@ -198,6 +200,8 @@ type MonthlyRevenueData = {
|
||||
month: string;
|
||||
total: number;
|
||||
net: number;
|
||||
net_gudang: number;
|
||||
net_ecer: number;
|
||||
deduction: number;
|
||||
};
|
||||
|
||||
@ -210,20 +214,57 @@ const revenueChartConfig = {
|
||||
label: 'Bersih',
|
||||
color: '#22c55e',
|
||||
},
|
||||
net_gudang: {
|
||||
label: 'Total Gudang',
|
||||
color: '#10b981',
|
||||
},
|
||||
net_ecer: {
|
||||
label: 'Total Ecer',
|
||||
color: '#06b6d4',
|
||||
},
|
||||
deduction: {
|
||||
label: 'Potongan',
|
||||
color: '#f97316',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const revenueTotals = computed(() => ({
|
||||
total: props.revenueSummary.total_revenue,
|
||||
net:
|
||||
props.revenueSummary.total_revenue -
|
||||
props.revenueSummary.total_deduction -
|
||||
props.profitMetrics.hpp,
|
||||
deduction: props.revenueSummary.total_deduction,
|
||||
}));
|
||||
const revenueTotals = computed(() => {
|
||||
const net_gudang = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_gudang ?? 0), 0);
|
||||
const net_ecer = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_ecer ?? 0), 0);
|
||||
return {
|
||||
total: props.revenueSummary.total_revenue,
|
||||
net:
|
||||
props.revenueSummary.total_revenue -
|
||||
props.revenueSummary.total_deduction -
|
||||
props.profitMetrics.hpp,
|
||||
net_gudang,
|
||||
net_ecer,
|
||||
deduction: props.revenueSummary.total_deduction,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleRevenueCharts = computed(() => {
|
||||
const isOwner = hasAnyRole(['owner', 'developer']);
|
||||
const isCashier = hasRole('cashier');
|
||||
|
||||
if (isOwner) {
|
||||
return ['total', 'net', 'net_gudang', 'net_ecer', 'deduction'] as const;
|
||||
}
|
||||
|
||||
if (isCashier) {
|
||||
return ['total', 'deduction'] as const;
|
||||
}
|
||||
|
||||
return ['total', 'net', 'deduction'] as const;
|
||||
});
|
||||
|
||||
const revenueYAccessors = computed(() => {
|
||||
return visibleRevenueCharts.value.map((chart) => (d: MonthlyRevenueData) => d[chart]);
|
||||
});
|
||||
|
||||
const revenueColors = computed(() => {
|
||||
return visibleRevenueCharts.value.map((chart) => revenueChartConfig[chart].color);
|
||||
});
|
||||
|
||||
// Expense Chart
|
||||
type MonthlyExpenseData = {
|
||||
@ -356,6 +397,7 @@ const barSelector = GroupedBar.selectors.bar;
|
||||
|
||||
function revenueTooltip(d: any) {
|
||||
const item = d as MonthlyRevenueData;
|
||||
const isOwner = hasAnyRole(['owner', 'developer']);
|
||||
const isCashier = hasRole('cashier');
|
||||
|
||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.6">
|
||||
@ -372,6 +414,18 @@ function revenueTooltip(d: any) {
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.net)}</span>
|
||||
</p>
|
||||
` : ''}
|
||||
${isOwner ? `
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.net_gudang.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.net_gudang.label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.net_gudang)}</span>
|
||||
</p>
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.net_ecer.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.net_ecer.label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.net_ecer)}</span>
|
||||
</p>
|
||||
` : ''}
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.deduction.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.deduction.label}</span>
|
||||
@ -655,9 +709,7 @@ watch([startDate, endDate], () => {
|
||||
<CardTitle>Pendapatan</CardTitle>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div v-for="chart in (
|
||||
!hasRole('cashier') ? ['total', 'net', 'deduction'] : ['total', 'deduction']
|
||||
) as const" :key="chart"
|
||||
<div v-for="chart in visibleRevenueCharts" :key="chart"
|
||||
class="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ revenueChartConfig[chart].label }}
|
||||
@ -671,9 +723,7 @@ watch([startDate, endDate], () => {
|
||||
<CardContent class="px-2 sm:p-6">
|
||||
<div v-if="monthlyRevenue.length > 0">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-center gap-4">
|
||||
<div v-for="chart in (
|
||||
!hasRole('cashier') ? ['total', 'net', 'deduction'] : ['total', 'deduction']
|
||||
) as const" :key="chart" class="flex items-center gap-1.5">
|
||||
<div v-for="chart in visibleRevenueCharts" :key="chart" class="flex items-center gap-1.5">
|
||||
<span class="size-2.5 rounded-full" :style="{
|
||||
backgroundColor:
|
||||
revenueChartConfig[chart].color,
|
||||
@ -686,15 +736,7 @@ watch([startDate, endDate], () => {
|
||||
<ChartContainer :config="revenueChartConfig" class="aspect-auto h-[300px] w-full">
|
||||
<VisXYContainer :data="monthlyRevenue" :y-domain="[0, undefined]">
|
||||
<VisGroupedBar :x="(_d: MonthlyRevenueData, i: number) => i
|
||||
" :y="[
|
||||
(d: MonthlyRevenueData) => d.total,
|
||||
...(!hasRole('cashier') ? [(d: MonthlyRevenueData) => d.net] : []),
|
||||
(d: MonthlyRevenueData) => d.deduction,
|
||||
]" :color="[
|
||||
revenueChartConfig.total.color,
|
||||
...(!hasRole('cashier') ? [revenueChartConfig.net.color] : []),
|
||||
revenueChartConfig.deduction.color,
|
||||
]" :bar-padding="0.1" :group-padding="0.2" :rounded-corners="4" />
|
||||
" :y="revenueYAccessors" :color="revenueColors" :bar-padding="0.1" :group-padding="0.2" :rounded-corners="4" />
|
||||
<VisAxis type="x" :x="(_d: MonthlyRevenueData, i: number) => i
|
||||
" :tick-line="false" :domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||
monthlyRevenue[d]?.month ?? ''
|
||||
|
||||
Loading…
Reference in New Issue
Block a user