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:
parent
5a96b48725
commit
15f2f0e1d0
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Requests\Concerns;
|
||||
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait HasRawMaterialPriceRules
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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]),
|
||||
);
|
||||
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user