feat: enhance order management by calculating and displaying total cost price and net amount in order summaries

This commit is contained in:
Yoga Pangestu 2026-07-01 11:45:25 +07:00
parent ce26551666
commit 60b358fb49
7 changed files with 139 additions and 24 deletions

View File

@ -64,6 +64,7 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
'createdBy.profile',
'items.productVariant.product:id,name',
'items.productVariant:id,product_id,name',
'items.productVariant.prices',
])
->when($user->hasAnyRole(['marketing-offline', 'marketing-online']), fn (Builder $query) => $query->where('marketing_id', $user->id))
->when($user->hasRole('cashier'), fn (Builder $query) => $query->where('created_by_id', $user->id))
@ -97,9 +98,11 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$order->setAttribute('is_editable', $order->status->isEditable());
$order->setAttribute(
'marketplace_settings_snapshot',
$this->enrichMarketplaceSnapshot($order->marketplace_settings_snapshot),
$this->enrichMarketplaceSnapshot($order->marketplace_settings_snapshot, $order),
);
$this->appendNetAmount($order);
return $order;
});
}
@ -242,6 +245,7 @@ public function findForShow(Order $order): Order
'marketing.profile',
'items.productVariant.product:id,name',
'items.productVariant.media',
'items.productVariant.prices',
'cashTransaction:id,amount,description,created_at',
'media',
]);
@ -272,9 +276,11 @@ public function findForShow(Order $order): Order
$order->setAttribute('is_editable', $order->status->isEditable());
$order->setAttribute(
'marketplace_settings_snapshot',
$this->enrichMarketplaceSnapshot($order->marketplace_settings_snapshot),
$this->enrichMarketplaceSnapshot($order->marketplace_settings_snapshot, $order),
);
$this->appendNetAmount($order);
return $order;
}
@ -886,16 +892,61 @@ private function presentVariantPricesFromCollection(int $variantId, Collection $
->all();
}
private function enrichMarketplaceSnapshot(?array $snapshot): ?array
private function enrichMarketplaceSnapshot(?array $snapshot, Order $order): ?array
{
if ($snapshot === null) {
return null;
}
$totalCostPrice = 0;
foreach ($order->items as $item) {
$variant = $item->productVariant;
if ($variant) {
$hargaModal = $variant->prices
->first(fn ($price) => $price->type === PriceType::HARGA_MODAL)
?->price ?? 0;
$totalCostPrice += $hargaModal * $item->quantity;
}
}
$netAmount = max(0, ($snapshot['net_amount'] ?? 0) - $totalCostPrice);
$snapshot['base_amount_formatted'] ??= 'Rp '.number_format($snapshot['base_amount'] ?? 0, 0, ',', '.');
$snapshot['total_fee_amount_formatted'] ??= 'Rp '.number_format($snapshot['total_fee_amount'] ?? 0, 0, ',', '.');
$snapshot['net_amount_formatted'] ??= 'Rp '.number_format($snapshot['net_amount'] ?? 0, 0, ',', '.');
$snapshot['total_cost_price'] = $totalCostPrice;
$snapshot['total_cost_price_formatted'] = 'Rp '.number_format($totalCostPrice, 0, ',', '.');
$snapshot['net_amount'] = $netAmount;
$snapshot['net_amount_formatted'] = 'Rp '.number_format($netAmount, 0, ',', '.');
return $snapshot;
}
private function appendNetAmount(Order $order): void
{
$totalCostPrice = 0;
foreach ($order->items as $item) {
$variant = $item->productVariant;
if ($variant) {
$hargaModal = $variant->prices
->first(fn ($price) => $price->type === PriceType::HARGA_MODAL)
?->price ?? 0;
$totalCostPrice += $hargaModal * $item->quantity;
}
}
$baseAmount = $order->total_amount;
$netAmount = max(0, $baseAmount - $totalCostPrice);
if ($order->marketplace_settings_snapshot !== null) {
$snapshot = $order->marketplace_settings_snapshot;
$netAmount = $snapshot['net_amount'] ?? 0;
}
$order->setAttribute('total_cost_price', $totalCostPrice);
$order->setAttribute('total_cost_price_formatted', 'Rp '.number_format($totalCostPrice, 0, ',', '.'));
$order->setAttribute('net_amount', $netAmount);
$order->setAttribute('net_amount_formatted', 'Rp '.number_format($netAmount, 0, ',', '.'));
}
}

View File

@ -223,11 +223,25 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate =
->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
$totalCostPrice = 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')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp')
->value('total_hpp');
return [
'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0),
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
'total_marketplace_fees' => $totalMarketplaceFees,
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
'total_cost_price' => (int) ($totalCostPrice ?? 0),
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees + (int) ($totalCostPrice ?? 0),
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
];
@ -269,6 +283,24 @@ 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()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->leftJoin('product_prices', function ($join) {
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw("
DATE_FORMAT(orders.created_at, '%Y-%m') as month_key,
COALESCE(SUM(order_items.quantity * product_prices.price), 0) as hpp
")
->groupBy('month_key')
->get()
->pluck('hpp', 'month_key');
if ($monthlyData->isEmpty()) {
return [];
}
@ -284,8 +316,9 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate =
$revenue = $monthlyData->firstWhere('month_key', $key);
$fees = $monthlyFees->get($key, 0);
$hppVal = (int) $monthlyHpp->get($key, 0);
$discount = (int) ($revenue->total_discount ?? 0);
$deduction = $discount + $fees;
$deduction = $discount + $fees + $hppVal;
$result[] = [
'month' => $monthLabel,
@ -459,15 +492,18 @@ public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = n
$totalQty = (int) ($itemsData->total_qty ?? 0);
$totalItems = (int) ($itemsData->total_items ?? 0);
// HPP from cutting_result_prices
// HPP from product_prices with type = 'harga_modal'
$hpp = OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->leftJoin('cutting_result_prices', 'order_items.product_variant_id', '=', 'cutting_result_prices.product_variant_id')
->leftJoin('product_prices', function ($join) {
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('COALESCE(SUM(order_items.quantity * cutting_result_prices.cost_per_unit), 0) as total_hpp')
->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp')
->value('total_hpp');
$totalHpp = (int) ($hpp ?? 0);

View File

@ -5,6 +5,7 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\PriceType;
use App\Enums\Role;
use App\Models\Attendance;
use App\Models\CashAccount;
@ -14,6 +15,7 @@
use App\Models\Expense;
use App\Models\LeaveRequest;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Purchase;
use App\Models\User;
use Carbon\Carbon;
@ -148,11 +150,25 @@ public function getRevenueSummary(): array
->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
$totalCostPrice = 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')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->whereDate('orders.created_at', Carbon::today())
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp')
->value('total_hpp');
return [
'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0),
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
'total_marketplace_fees' => $totalMarketplaceFees,
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
'total_cost_price' => (int) ($totalCostPrice ?? 0),
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees + (int) ($totalCostPrice ?? 0),
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
];

View File

@ -326,9 +326,13 @@ const chartsGridClass = computed(() => {
revenueSummary.total_deduction,
),
},
{
label: 'Harga Modal',
value: 'Rp' + formatRupiah(revenueSummary.total_cost_price ?? 0),
},
{
label: 'Potongan',
value: 'Rp' + formatRupiah(revenueSummary.total_deduction),
value: 'Rp' + formatRupiah(revenueSummary.total_marketplace_fees ?? 0),
},
{
label: 'Diskon',

View File

@ -324,17 +324,18 @@ const feeEntries = computed(() => {
</CardContent>
</Card>
<!-- Marketplace Fee Breakdown -->
<Card v-if="snapshot">
<!-- Fee & Profit Breakdown -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Receipt class="size-5" />
Rincian Biaya {{ snapshot.platform === 'tiktok' ? 'TikTok Shop' : 'Shopee' }}
<Badge v-if="snapshot.is_affiliate" variant="outline" class="ml-1">Afiliasi</Badge>
<span v-if="snapshot">Rincian Biaya {{ snapshot.platform === 'tiktok' ? 'TikTok Shop' : 'Shopee' }}</span>
<span v-else>Rincian Keuntungan</span>
<Badge v-if="snapshot?.is_affiliate" variant="outline" class="ml-1">Afiliasi</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div class="overflow-x-auto">
<div v-if="snapshot" class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@ -361,25 +362,28 @@ const feeEntries = computed(() => {
</TableRow>
</TableBody>
</Table>
<Separator class="my-4" />
</div>
<Separator class="my-4" />
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Dasar Perhitungan</span>
<span class="tabular-nums">{{ formatRp(snapshot.base_amount) }}</span>
<span class="tabular-nums">{{ formatRp(snapshot ? snapshot.base_amount : order.total_amount) }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Total Potongan</span>
<div v-if="snapshot" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Total Potongan Marketplace</span>
<span class="tabular-nums text-destructive">-{{ formatRp(snapshot.total_fee_amount)
}}</span>
</div>
<div v-if="order.total_cost_price > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Total Harga Modal</span>
<span class="tabular-nums text-destructive">-{{ order.total_cost_price_formatted }}</span>
</div>
<Separator />
<div class="flex items-center justify-between">
<span class="font-semibold">Pendapatan Bersih</span>
<span class="text-lg font-bold tabular-nums text-green-600">
{{ formatRp(snapshot.net_amount) }}
{{ order.net_amount_formatted }}
</span>
</div>
</div>

View File

@ -104,8 +104,8 @@ function rowNumber(index: number): number {
<span v-if="order.marketplace_settings_snapshot?.total_fee_amount" class="text-destructive">
Potongan Marketplace <strong class="text-destructive">-{{ order.marketplace_settings_snapshot.total_fee_amount_formatted }}</strong>
</span>
<span v-if="order.marketplace_settings_snapshot?.net_amount">
Total Bersih <strong class="text-green-600">{{ order.marketplace_settings_snapshot.net_amount_formatted }}</strong>
<span v-if="order.net_amount_formatted">
Total Bersih <strong class="text-green-600">{{ order.net_amount_formatted }}</strong>
</span>
</div>
<p v-if="order.notes" class="text-muted-foreground text-sm">

View File

@ -82,6 +82,10 @@ export type OrderListItem = {
};
items: OrderItemListItem[];
marketplace_settings_snapshot?: MarketplaceSettingsSnapshot | null;
total_cost_price?: number;
total_cost_price_formatted?: string;
net_amount?: number;
net_amount_formatted?: string;
};
export type OrderCartItem = {