feat: add marketing sales analysis feature; enhance AnalysisService to retrieve marketing sales data and update Analysis.vue to display marketing sales table with relevant metrics

This commit is contained in:
Yoga Pangestu 2026-07-02 13:11:18 +07:00
parent 5a96b48725
commit 15f2f0e1d0
19 changed files with 132 additions and 51 deletions

View File

@ -33,6 +33,7 @@ enum Permission: string
case ANALYSIS_TOP_SUPPLIERS = 'analysis.top_suppliers';
case ANALYSIS_TOP_CUSTOMERS = 'analysis.top_customers';
case ANALYSIS_TOP_PRODUCTS = 'analysis.top_products';
case ANALYSIS_MARKETING_SALES = 'analysis.marketing_sales';
case EMPLOYEES_VIEW = 'employees.view';
case EMPLOYEES_CREATE = 'employees.create';
@ -172,6 +173,7 @@ public function label(): string
self::ANALYSIS_TOP_SUPPLIERS => 'Lihat Top 5 Supplier',
self::ANALYSIS_TOP_CUSTOMERS => 'Lihat Top 5 Pelanggan',
self::ANALYSIS_TOP_PRODUCTS => 'Lihat Top 5 Produk',
self::ANALYSIS_MARKETING_SALES => 'Lihat Analisa Penjualan Marketing',
self::EMPLOYEES_VIEW => 'Lihat Pegawai',
self::EMPLOYEES_CREATE => 'Tambah Pegawai',
@ -295,7 +297,8 @@ public function group(): string
self::ANALYSIS_RAW_MATERIALS, self::ANALYSIS_PRODUCT_STOCK, self::ANALYSIS_REVENUE,
self::ANALYSIS_EXPENSE, self::ANALYSIS_BUSY_HOURS, self::ANALYSIS_PROFIT_ORDERS,
self::ANALYSIS_PROFIT_HPP, self::ANALYSIS_PROFIT_GROSS, self::ANALYSIS_PROFIT_MARGIN,
self::ANALYSIS_TOP_SUPPLIERS, self::ANALYSIS_TOP_CUSTOMERS, self::ANALYSIS_TOP_PRODUCTS => 'Umum',
self::ANALYSIS_TOP_SUPPLIERS, self::ANALYSIS_TOP_CUSTOMERS, self::ANALYSIS_TOP_PRODUCTS,
self::ANALYSIS_MARKETING_SALES => 'Umum',
self::EMPLOYEES_VIEW, self::EMPLOYEES_CREATE, self::EMPLOYEES_UPDATE,
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
self::ATTENDANCES_VIEW, self::ATTENDANCES_CREATE, self::ATTENDANCES_DELETE,

View File

@ -78,6 +78,7 @@ public function permissions(): array
Permission::ANALYSIS_PROFIT_MARGIN,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ANALYSIS_MARKETING_SALES,
Permission::EMPLOYEES_VIEW,
@ -130,9 +131,11 @@ public function permissions(): array
Permission::ANALYSIS_REVENUE,
Permission::ANALYSIS_EXPENSE,
Permission::ANALYSIS_BUSY_HOURS,
Permission::ANALYSIS_PROFIT_ORDERS,
Permission::ANALYSIS_TOP_SUPPLIERS,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ANALYSIS_MARKETING_SALES,
Permission::STOCKS_VIEW,

View File

@ -41,6 +41,7 @@ public function index(Request $request): Response
'topSuppliers' => $this->analysisService->getTopSuppliers($startDate, $endDate),
'topCustomers' => $this->analysisService->getTopCustomers($startDate, $endDate),
'topProducts' => $this->analysisService->getTopProducts($startDate, $endDate),
'marketingSales' => $this->analysisService->getMarketingSales($startDate, $endDate),
]);
}
}

View File

@ -2,7 +2,6 @@
namespace App\Http\Requests\Concerns;
use App\Enums\RawMaterialUnit;
use Illuminate\Validation\Rule;
trait HasRawMaterialPriceRules

View File

@ -97,7 +97,7 @@ public function create(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'💰 Pengajuan Kasbon Baru',
"Karyawan {$user->profil?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.employee_advances.index'),
);
}
@ -127,7 +127,7 @@ public function update(EmployeeAdvance $employeeAdvance, array $validated, User
$this->pushNotificationService->sendToRoles(
'✏️ Kasbon Diperbarui',
"Kasbon sebesar {$employeeAdvance->amount_formatted} telah diperbarui oleh {$user->profile?->full_name}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.employee_advances.index'),
);
}
@ -142,7 +142,7 @@ public function delete(EmployeeAdvance $employeeAdvance, User $user): void
$this->pushNotificationService->sendToRoles(
'🗑️ Kasbon Dihapus',
"Kasbon sebesar {$amount} dengan keterangan {$description} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.employee_advances.index'),
);
}

View File

@ -92,7 +92,7 @@ public function create(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'💸 Pengeluaran Baru',
"Pengeluaran baru sebesar {$expense->amount_formatted} dengan keterangan {$expense->description} telah dicatat oleh {$user->profile?->full_name}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.expenses.index'),
);
}
@ -134,7 +134,7 @@ public function update(Expense $expense, array $validated): void
$this->pushNotificationService->sendToRoles(
'✏️ Pengeluaran Diperbarui',
"Pengeluaran dengan keterangan {$expense->description} diperbarui menjadi sebesar {$expense->amount_formatted}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.expenses.index'),
);
}
@ -165,7 +165,7 @@ public function delete(Expense $expense): void
$this->pushNotificationService->sendToRoles(
'🗑️ Pengeluaran Dihapus',
"Pengeluaran sebesar {$expense->amount_formatted} dengan keterangan {$expense->description} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.finance.expenses.index'),
);
}

View File

@ -100,7 +100,7 @@ public function checkIn(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'⏰ Presensi Masuk',
"Karyawan {$user->profile?->full_name} melakukan presensi masuk.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.attendances.index'),
);
}
@ -146,7 +146,7 @@ public function checkOut(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'⏰ Presensi Pulang',
"Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).',
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.attendances.index'),
);
}
@ -164,7 +164,7 @@ public function delete(Attendance $attendance): void
$this->pushNotificationService->sendToRoles(
'🗑️ Presensi Dihapus',
"Data presensi {$employeeName} tanggal {$date} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.attendances.index'),
);
}

View File

@ -78,7 +78,7 @@ public function create(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'🏖️ Pengajuan Cuti Baru',
"Karyawan {$user->profile?->full_name} mengajukan cuti selama {$leaveRequest->total_days} hari mulai dari tanggal {$leaveRequest->start_date_formatted}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.leave_requests.index'),
);
}
@ -99,7 +99,7 @@ public function update(LeaveRequest $leaveRequest, array $validated, User $user)
$this->pushNotificationService->sendToRoles(
'✏️ Pengajuan Cuti Diperbarui',
"Pengajuan cuti oleh {$user->profile?->full_name} telah diperbarui.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.leave_requests.index'),
);
}
@ -114,7 +114,7 @@ public function delete(LeaveRequest $leaveRequest): void
$this->pushNotificationService->sendToRoles(
'🗑️ Pengajuan Cuti Dihapus',
"Pengajuan cuti {$totalDays} hari oleh {$employeeName} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.hr.leave_requests.index'),
);
}

View File

@ -455,7 +455,7 @@ public function create(array $validated, User $user): Cutting
$this->pushNotificationService->sendToRoles(
'✂️ Proses Cutting Baru',
"Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
@ -524,7 +524,7 @@ public function update(Cutting $cutting, array $validated): void
$this->pushNotificationService->sendToRoles(
'✏️ Proses Cutting Diperbarui',
"Proses cutting dengan deskripsi '{$description}' telah diperbarui.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
}
@ -568,7 +568,7 @@ public function delete(Cutting $cutting): void
$this->pushNotificationService->sendToRoles(
'🗑️ Proses Cutting Dihapus',
"Proses cutting dengan deskripsi {$description} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
}
@ -661,8 +661,8 @@ public function transitionStatus(
};
$roles = $status === CuttingStatus::COMPLETED
? ['owner', 'developer', 'admin-toko']
: ['owner', 'developer'];
? ['owner', 'developer', 'direktur', 'admin-toko']
: ['owner', 'developer', 'direktur'];
$this->pushNotificationService->sendToRoles(
$title,
@ -1105,7 +1105,7 @@ public function quickCreateProduct(array $validated): array
'is_active' => true,
]);
if (!empty($validated['category_ids'])) {
if (! empty($validated['category_ids'])) {
$product->categories()->sync($validated['category_ids']);
}
@ -1119,7 +1119,7 @@ public function quickCreateProduct(array $validated): array
'retail_stock' => $variantData['retail_stock'] ?? 0,
]);
if (!empty($variantData['prices'])) {
if (! empty($variantData['prices'])) {
foreach ($variantData['prices'] as $type => $priceValue) {
$variant->prices()->create([
'type' => $type,

View File

@ -498,7 +498,7 @@ public function create(array $validated, User $user): Order
$this->pushNotificationService->sendToRoles(
'📦 Pesanan Baru',
"Pesanan baru {$order->order_number} senilai {$order->total_amount_formatted} telah dibuat oleh {$user->profile?->full_name}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
@ -596,7 +596,7 @@ public function update(Order $order, array $validated): void
$this->pushNotificationService->sendToRoles(
'✏️ Pesanan Diperbarui',
"Pesanan {$order->order_number} senilai {$order->total_amount_formatted} telah diperbarui.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
}
@ -638,7 +638,7 @@ public function delete(Order $order): void
$this->pushNotificationService->sendToRoles(
'🗑️ Pesanan Dihapus',
"Pesanan {$orderNumber} senilai {$order->total_amount_formatted} telah dihapus.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
}
@ -684,7 +684,7 @@ public function transitionStatus(Order $order, OrderStatus $status): void
$this->pushNotificationService->sendToRoles(
'📦 Status Pesanan Diubah',
"Pesanan {$order->order_number} diubah statusnya menjadi {$status->label()}.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
}

View File

@ -265,7 +265,7 @@ public function rejectRequest(
$this->pushNotificationService->sendToRoles(
'❌ Pengajuan Ditolak',
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.dashboard'),
);

View File

@ -593,7 +593,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
$this->pushNotificationService->sendToRoles(
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
['owner', 'developer', 'direktur'],
$ownerUrl,
);

View File

@ -85,7 +85,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
$this->pushNotificationService->sendToRoles(
'📦 Transfer Stok Ecer Menunggu Persetujuan Owner',
"Pengajuan transfer {$quantity} pcs stok ecer untuk varian '{$variant->name}' menunggu verifikasi owner.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.master.products.index', ['search' => $productName]),
);

View File

@ -126,7 +126,7 @@ public function submitVerification(
$this->pushNotificationService->sendToRoles(
'📦 Verifikasi Stok Menunggu Persetujuan',
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.stocks.index'),
);
}
@ -176,7 +176,7 @@ public function approveVerification(
$this->pushNotificationService->sendToRoles(
'📦 Stok Cutting Diverifikasi',
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.stocks.index'),
);
}
@ -218,7 +218,7 @@ public function rejectVerification(
$this->pushNotificationService->sendToRoles(
'📦 Verifikasi Cutting Ditolak Owner',
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.manage.stocks.index'),
);

View File

@ -554,7 +554,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
$this->pushNotificationService->sendToRoles(
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
['owner', 'developer', 'direktur'],
$ownerUrl,
);

View File

@ -363,7 +363,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
$this->pushNotificationService->sendToRoles(
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
['owner', 'developer', 'direktur'],
$ownerUrl,
);

View File

@ -243,7 +243,7 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate =
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
'total_marketplace_fees' => $totalMarketplaceFees,
'total_cost_price' => (int) ($totalCostPrice ?? 0),
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees + (int) ($totalCostPrice ?? 0),
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
];
@ -318,9 +318,8 @@ 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 + $hppVal;
$deduction = $discount + $fees;
$result[] = [
'month' => $monthLabel,
@ -361,7 +360,7 @@ public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate =
->first();
$purchaseTotal = (int) ($purchase->total ?? 0);
if ($user?->hasRole(Role::ADMIN_TOKO->value)) {
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseTotal = 0;
}
$expenseTotal = (int) ($expenses->total ?? 0);
@ -428,7 +427,7 @@ public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate =
$monthLabel = $current->locale('id')->translatedFormat('M Y');
$purchaseAmount = (int) ($purchases->firstWhere('month_key', $key)->total ?? 0);
if ($user?->hasRole(Role::ADMIN_TOKO->value)) {
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseAmount = 0;
}
$expenseAmount = (int) ($expenses->firstWhere('month_key', $key)->total ?? 0);
@ -518,7 +517,7 @@ public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = n
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
->sum('total');
if ($user?->hasRole(Role::ADMIN_TOKO->value)) {
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseTotal = 0;
}
@ -536,7 +535,7 @@ public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = n
$totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal;
$labaKotor = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees;
$labaBersih = $labaKotor - $totalExpenses;
$labaBersih = $labaKotor - (int) $expenseTotal - (int) $advanceTotal;
$profitMargin = $totalRevenue > 0 ? round(($labaBersih / $totalRevenue) * 100, 1) : 0;
$aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0;
$itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0;
@ -672,6 +671,37 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
->toArray();
}
public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return Order::query()
->completed()
->whereNotNull('marketing_id')
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->join('users', 'orders.marketing_id', '=', 'users.id')
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
->selectRaw('
users.id as marketing_id,
COALESCE(user_profiles.full_name, users.username) as marketing_name,
COUNT(*) as total_orders,
SUM(orders.total_amount) as total_revenue,
SUM(orders.subtotal) as total_subtotal,
SUM(orders.discount) as total_discount,
AVG(orders.total_amount) as avg_order
')
->groupBy('users.id', 'user_profiles.full_name', 'users.username')
->orderByDesc('total_revenue')
->get()
->map(fn ($item) => [
'marketing_name' => $item->marketing_name,
'total_orders' => (int) $item->total_orders,
'total_revenue' => (int) $item->total_revenue,
'total_subtotal' => (int) $item->total_subtotal,
'total_discount' => (int) $item->total_discount,
'avg_order' => (int) $item->avg_order,
])
->toArray();
}
public function isManager(?User $user): bool
{
return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;

View File

@ -118,7 +118,7 @@ public function updateMarketplace(array $validated, User $user): void
$this->pushNotificationService->sendToRoles(
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
['owner', 'developer'],
['owner', 'developer', 'direktur'],
route('admin.system.settings.index'),
);

View File

@ -22,7 +22,7 @@ import AdminLayout from '@/layouts/AdminLayout.vue';
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
import admin from '@/routes/admin';
const { can, hasRole } = useCan();
const { can, hasRole, hasAnyRole } = useCan();
const props = defineProps<{
filters: {
@ -123,6 +123,14 @@ const props = defineProps<{
total_qty: number;
total_revenue: number;
}>;
marketingSales: Array<{
marketing_name: string;
total_orders: number;
total_revenue: number;
total_subtotal: number;
total_discount: number;
avg_order: number;
}>;
}>();
const startDate = ref(props.filters.start_date ?? '');
@ -222,17 +230,18 @@ const expenseTotals = computed(() => ({
const visibleExpenseCharts = computed(() => {
const charts = ['total', 'purchase', 'expense', 'advance'] as const;
if (hasRole('admin-toko')) {
return charts.filter((c) => c !== 'purchase');
if (hasAnyRole(['owner', 'developer'])) {
return charts;
}
return charts;
return charts.filter((c) => c !== 'purchase');
});
const expenseYAccessors = computed(() => {
if (hasRole('admin-toko')) {
if (hasAnyRole(['owner', 'developer'])) {
return [
(d: MonthlyExpenseData) => d.total,
(d: MonthlyExpenseData) => d.purchase,
(d: MonthlyExpenseData) => d.expense,
(d: MonthlyExpenseData) => d.advance,
];
@ -240,16 +249,16 @@ const expenseYAccessors = computed(() => {
return [
(d: MonthlyExpenseData) => d.total,
(d: MonthlyExpenseData) => d.purchase,
(d: MonthlyExpenseData) => d.expense,
(d: MonthlyExpenseData) => d.advance,
];
});
const expenseColors = computed(() => {
if (hasRole('admin-toko')) {
if (hasAnyRole(['owner', 'developer'])) {
return [
expenseChartConfig.total.color,
expenseChartConfig.purchase.color,
expenseChartConfig.expense.color,
expenseChartConfig.advance.color,
];
@ -257,7 +266,6 @@ const expenseColors = computed(() => {
return [
expenseChartConfig.total.color,
expenseChartConfig.purchase.color,
expenseChartConfig.expense.color,
expenseChartConfig.advance.color,
];
@ -739,8 +747,8 @@ watch([startDate, endDate], () => {
value: 'Rp' + formatRupiah(revenueSummary.total_revenue),
},
{
label: 'Pengeluaran',
value: 'Rp' + formatRupiah(expenseSummary.total),
label: 'HPP',
value: 'Rp' + formatRupiah(profitMetrics.hpp),
},
{
label: 'Laba Bersih',
@ -832,6 +840,43 @@ watch([startDate, endDate], () => {
</CardContent>
</Card>
</div>
<!-- Marketing Sales Table -->
<Card v-if="can('analysis.marketing_sales')">
<CardHeader>
<CardTitle>Penjualan Marketing</CardTitle>
<CardDescription>Rekap penjualan per marketing</CardDescription>
</CardHeader>
<CardContent>
<div v-if="marketingSales.length > 0" class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b">
<th class="py-3 px-4 text-left font-medium text-muted-foreground">Marketing</th>
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Order</th>
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Pendapatan</th>
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Subtotal</th>
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Diskon</th>
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Rata-rata Order</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in marketingSales" :key="index" class="border-b last:border-0">
<td class="py-3 px-4 font-medium">{{ item.marketing_name }}</td>
<td class="py-3 px-4 text-right tabular-nums">{{ item.total_orders.toLocaleString('id-ID') }}</td>
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_revenue) }}</td>
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_subtotal) }}</td>
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_discount) }}</td>
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.avg_order) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="flex h-[150px] items-center justify-center text-muted-foreground">
Belum ada data penjualan marketing
</div>
</CardContent>
</Card>
</div>
</AdminLayout>
</template>