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_SUPPLIERS = 'analysis.top_suppliers';
|
||||||
case ANALYSIS_TOP_CUSTOMERS = 'analysis.top_customers';
|
case ANALYSIS_TOP_CUSTOMERS = 'analysis.top_customers';
|
||||||
case ANALYSIS_TOP_PRODUCTS = 'analysis.top_products';
|
case ANALYSIS_TOP_PRODUCTS = 'analysis.top_products';
|
||||||
|
case ANALYSIS_MARKETING_SALES = 'analysis.marketing_sales';
|
||||||
|
|
||||||
case EMPLOYEES_VIEW = 'employees.view';
|
case EMPLOYEES_VIEW = 'employees.view';
|
||||||
case EMPLOYEES_CREATE = 'employees.create';
|
case EMPLOYEES_CREATE = 'employees.create';
|
||||||
@ -172,6 +173,7 @@ public function label(): string
|
|||||||
self::ANALYSIS_TOP_SUPPLIERS => 'Lihat Top 5 Supplier',
|
self::ANALYSIS_TOP_SUPPLIERS => 'Lihat Top 5 Supplier',
|
||||||
self::ANALYSIS_TOP_CUSTOMERS => 'Lihat Top 5 Pelanggan',
|
self::ANALYSIS_TOP_CUSTOMERS => 'Lihat Top 5 Pelanggan',
|
||||||
self::ANALYSIS_TOP_PRODUCTS => 'Lihat Top 5 Produk',
|
self::ANALYSIS_TOP_PRODUCTS => 'Lihat Top 5 Produk',
|
||||||
|
self::ANALYSIS_MARKETING_SALES => 'Lihat Analisa Penjualan Marketing',
|
||||||
|
|
||||||
self::EMPLOYEES_VIEW => 'Lihat Pegawai',
|
self::EMPLOYEES_VIEW => 'Lihat Pegawai',
|
||||||
self::EMPLOYEES_CREATE => 'Tambah 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_RAW_MATERIALS, self::ANALYSIS_PRODUCT_STOCK, self::ANALYSIS_REVENUE,
|
||||||
self::ANALYSIS_EXPENSE, self::ANALYSIS_BUSY_HOURS, self::ANALYSIS_PROFIT_ORDERS,
|
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_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_VIEW, self::EMPLOYEES_CREATE, self::EMPLOYEES_UPDATE,
|
||||||
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
||||||
self::ATTENDANCES_VIEW, self::ATTENDANCES_CREATE, self::ATTENDANCES_DELETE,
|
self::ATTENDANCES_VIEW, self::ATTENDANCES_CREATE, self::ATTENDANCES_DELETE,
|
||||||
|
|||||||
@ -78,6 +78,7 @@ public function permissions(): array
|
|||||||
Permission::ANALYSIS_PROFIT_MARGIN,
|
Permission::ANALYSIS_PROFIT_MARGIN,
|
||||||
Permission::ANALYSIS_TOP_CUSTOMERS,
|
Permission::ANALYSIS_TOP_CUSTOMERS,
|
||||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||||
|
Permission::ANALYSIS_MARKETING_SALES,
|
||||||
|
|
||||||
Permission::EMPLOYEES_VIEW,
|
Permission::EMPLOYEES_VIEW,
|
||||||
|
|
||||||
@ -130,9 +131,11 @@ public function permissions(): array
|
|||||||
Permission::ANALYSIS_REVENUE,
|
Permission::ANALYSIS_REVENUE,
|
||||||
Permission::ANALYSIS_EXPENSE,
|
Permission::ANALYSIS_EXPENSE,
|
||||||
Permission::ANALYSIS_BUSY_HOURS,
|
Permission::ANALYSIS_BUSY_HOURS,
|
||||||
|
Permission::ANALYSIS_PROFIT_ORDERS,
|
||||||
Permission::ANALYSIS_TOP_SUPPLIERS,
|
Permission::ANALYSIS_TOP_SUPPLIERS,
|
||||||
Permission::ANALYSIS_TOP_CUSTOMERS,
|
Permission::ANALYSIS_TOP_CUSTOMERS,
|
||||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||||
|
Permission::ANALYSIS_MARKETING_SALES,
|
||||||
|
|
||||||
Permission::STOCKS_VIEW,
|
Permission::STOCKS_VIEW,
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ public function index(Request $request): Response
|
|||||||
'topSuppliers' => $this->analysisService->getTopSuppliers($startDate, $endDate),
|
'topSuppliers' => $this->analysisService->getTopSuppliers($startDate, $endDate),
|
||||||
'topCustomers' => $this->analysisService->getTopCustomers($startDate, $endDate),
|
'topCustomers' => $this->analysisService->getTopCustomers($startDate, $endDate),
|
||||||
'topProducts' => $this->analysisService->getTopProducts($startDate, $endDate),
|
'topProducts' => $this->analysisService->getTopProducts($startDate, $endDate),
|
||||||
|
'marketingSales' => $this->analysisService->getMarketingSales($startDate, $endDate),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Concerns;
|
namespace App\Http\Requests\Concerns;
|
||||||
|
|
||||||
use App\Enums\RawMaterialUnit;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
trait HasRawMaterialPriceRules
|
trait HasRawMaterialPriceRules
|
||||||
|
|||||||
@ -97,7 +97,7 @@ public function create(array $validated, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'💰 Pengajuan Kasbon Baru',
|
'💰 Pengajuan Kasbon Baru',
|
||||||
"Karyawan {$user->profil?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.",
|
"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'),
|
route('admin.finance.employee_advances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ public function update(EmployeeAdvance $employeeAdvance, array $validated, User
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✏️ Kasbon Diperbarui',
|
'✏️ Kasbon Diperbarui',
|
||||||
"Kasbon sebesar {$employeeAdvance->amount_formatted} telah diperbarui oleh {$user->profile?->full_name}.",
|
"Kasbon sebesar {$employeeAdvance->amount_formatted} telah diperbarui oleh {$user->profile?->full_name}.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.finance.employee_advances.index'),
|
route('admin.finance.employee_advances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -142,7 +142,7 @@ public function delete(EmployeeAdvance $employeeAdvance, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Kasbon Dihapus',
|
'🗑️ Kasbon Dihapus',
|
||||||
"Kasbon sebesar {$amount} dengan keterangan {$description} telah dihapus.",
|
"Kasbon sebesar {$amount} dengan keterangan {$description} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.finance.employee_advances.index'),
|
route('admin.finance.employee_advances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,7 @@ public function create(array $validated, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'💸 Pengeluaran Baru',
|
'💸 Pengeluaran Baru',
|
||||||
"Pengeluaran baru sebesar {$expense->amount_formatted} dengan keterangan {$expense->description} telah dicatat oleh {$user->profile?->full_name}.",
|
"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'),
|
route('admin.finance.expenses.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@ public function update(Expense $expense, array $validated): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✏️ Pengeluaran Diperbarui',
|
'✏️ Pengeluaran Diperbarui',
|
||||||
"Pengeluaran dengan keterangan {$expense->description} diperbarui menjadi sebesar {$expense->amount_formatted}.",
|
"Pengeluaran dengan keterangan {$expense->description} diperbarui menjadi sebesar {$expense->amount_formatted}.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.finance.expenses.index'),
|
route('admin.finance.expenses.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -165,7 +165,7 @@ public function delete(Expense $expense): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Pengeluaran Dihapus',
|
'🗑️ Pengeluaran Dihapus',
|
||||||
"Pengeluaran sebesar {$expense->amount_formatted} dengan keterangan {$expense->description} telah dihapus.",
|
"Pengeluaran sebesar {$expense->amount_formatted} dengan keterangan {$expense->description} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.finance.expenses.index'),
|
route('admin.finance.expenses.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,7 +100,7 @@ public function checkIn(array $validated, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'⏰ Presensi Masuk',
|
'⏰ Presensi Masuk',
|
||||||
"Karyawan {$user->profile?->full_name} melakukan presensi masuk.",
|
"Karyawan {$user->profile?->full_name} melakukan presensi masuk.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.hr.attendances.index'),
|
route('admin.hr.attendances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -146,7 +146,7 @@ public function checkOut(array $validated, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'⏰ Presensi Pulang',
|
'⏰ Presensi Pulang',
|
||||||
"Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).',
|
"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'),
|
route('admin.hr.attendances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -164,7 +164,7 @@ public function delete(Attendance $attendance): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Presensi Dihapus',
|
'🗑️ Presensi Dihapus',
|
||||||
"Data presensi {$employeeName} tanggal {$date} telah dihapus.",
|
"Data presensi {$employeeName} tanggal {$date} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.hr.attendances.index'),
|
route('admin.hr.attendances.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public function create(array $validated, User $user): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🏖️ Pengajuan Cuti Baru',
|
'🏖️ Pengajuan Cuti Baru',
|
||||||
"Karyawan {$user->profile?->full_name} mengajukan cuti selama {$leaveRequest->total_days} hari mulai dari tanggal {$leaveRequest->start_date_formatted}.",
|
"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'),
|
route('admin.hr.leave_requests.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ public function update(LeaveRequest $leaveRequest, array $validated, User $user)
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✏️ Pengajuan Cuti Diperbarui',
|
'✏️ Pengajuan Cuti Diperbarui',
|
||||||
"Pengajuan cuti oleh {$user->profile?->full_name} telah diperbarui.",
|
"Pengajuan cuti oleh {$user->profile?->full_name} telah diperbarui.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.hr.leave_requests.index'),
|
route('admin.hr.leave_requests.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ public function delete(LeaveRequest $leaveRequest): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Pengajuan Cuti Dihapus',
|
'🗑️ Pengajuan Cuti Dihapus',
|
||||||
"Pengajuan cuti {$totalDays} hari oleh {$employeeName} telah dihapus.",
|
"Pengajuan cuti {$totalDays} hari oleh {$employeeName} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.hr.leave_requests.index'),
|
route('admin.hr.leave_requests.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -455,7 +455,7 @@ public function create(array $validated, User $user): Cutting
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✂️ Proses Cutting Baru',
|
'✂️ Proses Cutting Baru',
|
||||||
"Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
|
"Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.cuttings.index'),
|
route('admin.manage.cuttings.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -524,7 +524,7 @@ public function update(Cutting $cutting, array $validated): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✏️ Proses Cutting Diperbarui',
|
'✏️ Proses Cutting Diperbarui',
|
||||||
"Proses cutting dengan deskripsi '{$description}' telah diperbarui.",
|
"Proses cutting dengan deskripsi '{$description}' telah diperbarui.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.cuttings.index'),
|
route('admin.manage.cuttings.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -568,7 +568,7 @@ public function delete(Cutting $cutting): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Proses Cutting Dihapus',
|
'🗑️ Proses Cutting Dihapus',
|
||||||
"Proses cutting dengan deskripsi {$description} telah dihapus.",
|
"Proses cutting dengan deskripsi {$description} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.cuttings.index'),
|
route('admin.manage.cuttings.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -661,8 +661,8 @@ public function transitionStatus(
|
|||||||
};
|
};
|
||||||
|
|
||||||
$roles = $status === CuttingStatus::COMPLETED
|
$roles = $status === CuttingStatus::COMPLETED
|
||||||
? ['owner', 'developer', 'admin-toko']
|
? ['owner', 'developer', 'direktur', 'admin-toko']
|
||||||
: ['owner', 'developer'];
|
: ['owner', 'developer', 'direktur'];
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
$title,
|
$title,
|
||||||
@ -1105,7 +1105,7 @@ public function quickCreateProduct(array $validated): array
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($validated['category_ids'])) {
|
if (! empty($validated['category_ids'])) {
|
||||||
$product->categories()->sync($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,
|
'retail_stock' => $variantData['retail_stock'] ?? 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($variantData['prices'])) {
|
if (! empty($variantData['prices'])) {
|
||||||
foreach ($variantData['prices'] as $type => $priceValue) {
|
foreach ($variantData['prices'] as $type => $priceValue) {
|
||||||
$variant->prices()->create([
|
$variant->prices()->create([
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
|
|||||||
@ -498,7 +498,7 @@ public function create(array $validated, User $user): Order
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Pesanan Baru',
|
'📦 Pesanan Baru',
|
||||||
"Pesanan baru {$order->order_number} senilai {$order->total_amount_formatted} telah dibuat oleh {$user->profile?->full_name}.",
|
"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'),
|
route('admin.manage.orders.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -596,7 +596,7 @@ public function update(Order $order, array $validated): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✏️ Pesanan Diperbarui',
|
'✏️ Pesanan Diperbarui',
|
||||||
"Pesanan {$order->order_number} senilai {$order->total_amount_formatted} telah diperbarui.",
|
"Pesanan {$order->order_number} senilai {$order->total_amount_formatted} telah diperbarui.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.orders.index'),
|
route('admin.manage.orders.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -638,7 +638,7 @@ public function delete(Order $order): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Pesanan Dihapus',
|
'🗑️ Pesanan Dihapus',
|
||||||
"Pesanan {$orderNumber} senilai {$order->total_amount_formatted} telah dihapus.",
|
"Pesanan {$orderNumber} senilai {$order->total_amount_formatted} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.orders.index'),
|
route('admin.manage.orders.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -684,7 +684,7 @@ public function transitionStatus(Order $order, OrderStatus $status): void
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Status Pesanan Diubah',
|
'📦 Status Pesanan Diubah',
|
||||||
"Pesanan {$order->order_number} diubah statusnya menjadi {$status->label()}.",
|
"Pesanan {$order->order_number} diubah statusnya menjadi {$status->label()}.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.orders.index'),
|
route('admin.manage.orders.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -265,7 +265,7 @@ public function rejectRequest(
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'❌ Pengajuan Ditolak',
|
'❌ Pengajuan Ditolak',
|
||||||
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
|
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.dashboard'),
|
route('admin.dashboard'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -593,7 +593,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
$ownerUrl,
|
$ownerUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -85,7 +85,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Transfer Stok Ecer Menunggu Persetujuan Owner',
|
'📦 Transfer Stok Ecer Menunggu Persetujuan Owner',
|
||||||
"Pengajuan transfer {$quantity} pcs stok ecer untuk varian '{$variant->name}' menunggu verifikasi 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]),
|
route('admin.master.products.index', ['search' => $productName]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,7 @@ public function submitVerification(
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Verifikasi Stok Menunggu Persetujuan',
|
'📦 Verifikasi Stok Menunggu Persetujuan',
|
||||||
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
|
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.stocks.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ public function approveVerification(
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Stok Cutting Diverifikasi',
|
'📦 Stok Cutting Diverifikasi',
|
||||||
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
|
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.stocks.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -218,7 +218,7 @@ public function rejectVerification(
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'📦 Verifikasi Cutting Ditolak Owner',
|
'📦 Verifikasi Cutting Ditolak Owner',
|
||||||
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
|
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.manage.stocks.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -554,7 +554,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
$ownerUrl,
|
$ownerUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -363,7 +363,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
$ownerUrl,
|
$ownerUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -243,7 +243,7 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate =
|
|||||||
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
|
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
|
||||||
'total_marketplace_fees' => $totalMarketplaceFees,
|
'total_marketplace_fees' => $totalMarketplaceFees,
|
||||||
'total_cost_price' => (int) ($totalCostPrice ?? 0),
|
'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),
|
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
|
||||||
'avg_order' => (int) ($revenueSummary->avg_order ?? 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);
|
$revenue = $monthlyData->firstWhere('month_key', $key);
|
||||||
$fees = $monthlyFees->get($key, 0);
|
$fees = $monthlyFees->get($key, 0);
|
||||||
$hppVal = (int) $monthlyHpp->get($key, 0);
|
|
||||||
$discount = (int) ($revenue->total_discount ?? 0);
|
$discount = (int) ($revenue->total_discount ?? 0);
|
||||||
$deduction = $discount + $fees + $hppVal;
|
$deduction = $discount + $fees;
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'month' => $monthLabel,
|
'month' => $monthLabel,
|
||||||
@ -361,7 +360,7 @@ public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate =
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
$purchaseTotal = (int) ($purchase->total ?? 0);
|
$purchaseTotal = (int) ($purchase->total ?? 0);
|
||||||
if ($user?->hasRole(Role::ADMIN_TOKO->value)) {
|
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
|
||||||
$purchaseTotal = 0;
|
$purchaseTotal = 0;
|
||||||
}
|
}
|
||||||
$expenseTotal = (int) ($expenses->total ?? 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');
|
$monthLabel = $current->locale('id')->translatedFormat('M Y');
|
||||||
|
|
||||||
$purchaseAmount = (int) ($purchases->firstWhere('month_key', $key)->total ?? 0);
|
$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;
|
$purchaseAmount = 0;
|
||||||
}
|
}
|
||||||
$expenseAmount = (int) ($expenses->firstWhere('month_key', $key)->total ?? 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))
|
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
|
||||||
->sum('total');
|
->sum('total');
|
||||||
|
|
||||||
if ($user?->hasRole(Role::ADMIN_TOKO->value)) {
|
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
|
||||||
$purchaseTotal = 0;
|
$purchaseTotal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,7 +535,7 @@ public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = n
|
|||||||
$totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal;
|
$totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal;
|
||||||
|
|
||||||
$labaKotor = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees;
|
$labaKotor = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees;
|
||||||
$labaBersih = $labaKotor - $totalExpenses;
|
$labaBersih = $labaKotor - (int) $expenseTotal - (int) $advanceTotal;
|
||||||
$profitMargin = $totalRevenue > 0 ? round(($labaBersih / $totalRevenue) * 100, 1) : 0;
|
$profitMargin = $totalRevenue > 0 ? round(($labaBersih / $totalRevenue) * 100, 1) : 0;
|
||||||
$aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0;
|
$aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0;
|
||||||
$itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0;
|
$itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0;
|
||||||
@ -672,6 +671,37 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
|
|||||||
->toArray();
|
->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
|
public function isManager(?User $user): bool
|
||||||
{
|
{
|
||||||
return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
|
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(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
|
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
|
||||||
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
|
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.system.settings.index'),
|
route('admin.system.settings.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import AdminLayout from '@/layouts/AdminLayout.vue';
|
|||||||
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
||||||
import admin from '@/routes/admin';
|
import admin from '@/routes/admin';
|
||||||
|
|
||||||
const { can, hasRole } = useCan();
|
const { can, hasRole, hasAnyRole } = useCan();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
filters: {
|
filters: {
|
||||||
@ -123,6 +123,14 @@ const props = defineProps<{
|
|||||||
total_qty: number;
|
total_qty: number;
|
||||||
total_revenue: 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 ?? '');
|
const startDate = ref(props.filters.start_date ?? '');
|
||||||
@ -222,17 +230,18 @@ const expenseTotals = computed(() => ({
|
|||||||
const visibleExpenseCharts = computed(() => {
|
const visibleExpenseCharts = computed(() => {
|
||||||
const charts = ['total', 'purchase', 'expense', 'advance'] as const;
|
const charts = ['total', 'purchase', 'expense', 'advance'] as const;
|
||||||
|
|
||||||
if (hasRole('admin-toko')) {
|
if (hasAnyRole(['owner', 'developer'])) {
|
||||||
return charts.filter((c) => c !== 'purchase');
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return charts;
|
return charts.filter((c) => c !== 'purchase');
|
||||||
});
|
});
|
||||||
|
|
||||||
const expenseYAccessors = computed(() => {
|
const expenseYAccessors = computed(() => {
|
||||||
if (hasRole('admin-toko')) {
|
if (hasAnyRole(['owner', 'developer'])) {
|
||||||
return [
|
return [
|
||||||
(d: MonthlyExpenseData) => d.total,
|
(d: MonthlyExpenseData) => d.total,
|
||||||
|
(d: MonthlyExpenseData) => d.purchase,
|
||||||
(d: MonthlyExpenseData) => d.expense,
|
(d: MonthlyExpenseData) => d.expense,
|
||||||
(d: MonthlyExpenseData) => d.advance,
|
(d: MonthlyExpenseData) => d.advance,
|
||||||
];
|
];
|
||||||
@ -240,16 +249,16 @@ const expenseYAccessors = computed(() => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
(d: MonthlyExpenseData) => d.total,
|
(d: MonthlyExpenseData) => d.total,
|
||||||
(d: MonthlyExpenseData) => d.purchase,
|
|
||||||
(d: MonthlyExpenseData) => d.expense,
|
(d: MonthlyExpenseData) => d.expense,
|
||||||
(d: MonthlyExpenseData) => d.advance,
|
(d: MonthlyExpenseData) => d.advance,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const expenseColors = computed(() => {
|
const expenseColors = computed(() => {
|
||||||
if (hasRole('admin-toko')) {
|
if (hasAnyRole(['owner', 'developer'])) {
|
||||||
return [
|
return [
|
||||||
expenseChartConfig.total.color,
|
expenseChartConfig.total.color,
|
||||||
|
expenseChartConfig.purchase.color,
|
||||||
expenseChartConfig.expense.color,
|
expenseChartConfig.expense.color,
|
||||||
expenseChartConfig.advance.color,
|
expenseChartConfig.advance.color,
|
||||||
];
|
];
|
||||||
@ -257,7 +266,6 @@ const expenseColors = computed(() => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
expenseChartConfig.total.color,
|
expenseChartConfig.total.color,
|
||||||
expenseChartConfig.purchase.color,
|
|
||||||
expenseChartConfig.expense.color,
|
expenseChartConfig.expense.color,
|
||||||
expenseChartConfig.advance.color,
|
expenseChartConfig.advance.color,
|
||||||
];
|
];
|
||||||
@ -739,8 +747,8 @@ watch([startDate, endDate], () => {
|
|||||||
value: 'Rp' + formatRupiah(revenueSummary.total_revenue),
|
value: 'Rp' + formatRupiah(revenueSummary.total_revenue),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pengeluaran',
|
label: 'HPP',
|
||||||
value: 'Rp' + formatRupiah(expenseSummary.total),
|
value: 'Rp' + formatRupiah(profitMetrics.hpp),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Laba Bersih',
|
label: 'Laba Bersih',
|
||||||
@ -832,6 +840,43 @@ watch([startDate, endDate], () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user