feat: Enhance leave request and transaction management with user-specific data
- Added current user ID to leave request index for better context. - Updated transaction management to include user-specific filters and summaries. - Enhanced analysis service methods to incorporate user context for attendance and revenue metrics. - Modified dashboard service to reflect user-specific attendance and financial summaries. - Implemented role-based access control in leave request service for update and delete actions. - Adjusted frontend components to utilize user-specific data for leave requests and transactions. - Removed unused permissions from role seeder for cleaner access control.
This commit is contained in:
parent
5e07a1b910
commit
8053ea2db4
@ -29,6 +29,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'filterOptions' => [
|
||||
'statusOptions' => LeaveRequestStatus::toSelect(),
|
||||
],
|
||||
'currentUserId' => $request->user()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -28,13 +28,15 @@ public function __construct(
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$filters = $request->only(['status', 'channel', 'payment_type', 'customer_id', 'marketing_id', 'created_by_id', 'date_from', 'date_to']);
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('admin/manage/transaction/index', [
|
||||
'transactions' => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
filters: $filters,
|
||||
user: $user,
|
||||
),
|
||||
'summary' => $this->service->getSummary($filters),
|
||||
'summary' => $this->service->getSummary($filters, $user),
|
||||
'filters' => $filters,
|
||||
'filterOptions' => $this->service->getFilterOptions(),
|
||||
]);
|
||||
|
||||
@ -25,25 +25,25 @@ public function index(Request $request): Response
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
$attendance = $this->service->getAttendanceStats($startDate, $endDate);
|
||||
$attendance = $this->service->getAttendanceStats($startDate, $endDate, $user);
|
||||
$myAttendance = $this->service->getMyAttendance($user, $startDate, $endDate);
|
||||
$cashOverview = $this->service->getCashOverview($startDate, $endDate);
|
||||
$rawMaterialStock = $this->service->getRawMaterialStock();
|
||||
$productStock = $this->service->getProductStock();
|
||||
$revenueSummary = $this->service->getRevenueSummary($startDate, $endDate);
|
||||
$monthlyRevenue = $this->service->getMonthlyRevenue($startDate, $endDate);
|
||||
$monthlyRevenueByChannel = $this->service->getMonthlyRevenueByChannel($startDate, $endDate);
|
||||
$revenueByPaymentType = $this->service->getRevenueByPaymentType($startDate, $endDate);
|
||||
$expenseSummary = $this->service->getExpenseSummary($startDate, $endDate);
|
||||
$monthlyExpense = $this->service->getMonthlyExpense($startDate, $endDate);
|
||||
$busyHours = $this->service->getBusyHours($startDate, $endDate);
|
||||
$profitMetrics = $this->service->getProfitMetrics($startDate, $endDate);
|
||||
$revenueSummary = $this->service->getRevenueSummary($startDate, $endDate, $user);
|
||||
$monthlyRevenue = $this->service->getMonthlyRevenue($startDate, $endDate, $user);
|
||||
$monthlyRevenueByChannel = $this->service->getMonthlyRevenueByChannel($startDate, $endDate, $user);
|
||||
$revenueByPaymentType = $this->service->getRevenueByPaymentType($startDate, $endDate, $user);
|
||||
$expenseSummary = $this->service->getExpenseSummary($startDate, $endDate, $user);
|
||||
$monthlyExpense = $this->service->getMonthlyExpense($startDate, $endDate, $user);
|
||||
$busyHours = $this->service->getBusyHours($startDate, $endDate, $user);
|
||||
$profitMetrics = $this->service->getProfitMetrics($startDate, $endDate, $user);
|
||||
$topSuppliers = $this->service->getTopSuppliers($startDate, $endDate);
|
||||
$topCustomers = $this->service->getTopCustomers($startDate, $endDate);
|
||||
$topProducts = $this->service->getTopProducts($startDate, $endDate);
|
||||
$marketingSales = $this->service->getMarketingSales($startDate, $endDate);
|
||||
$orderStats = $this->service->getOrderStats($startDate, $endDate);
|
||||
$revenueTrend = $this->service->getRevenueTrend($startDate, $endDate);
|
||||
$topCustomers = $this->service->getTopCustomers($startDate, $endDate, $user);
|
||||
$topProducts = $this->service->getTopProducts($startDate, $endDate, $user);
|
||||
$marketingSales = $this->service->getMarketingSales($startDate, $endDate, $user);
|
||||
$orderStats = $this->service->getOrderStats($startDate, $endDate, $user);
|
||||
$revenueTrend = $this->service->getRevenueTrend($startDate, $endDate, $user);
|
||||
|
||||
return Inertia::render('admin/analysis/index', [
|
||||
'filters' => [
|
||||
|
||||
@ -18,10 +18,10 @@ public function __invoke(Request $request): Response
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('dashboard', [
|
||||
'attendance' => $this->service->getAttendanceStats(),
|
||||
'revenueSummary' => $this->service->getRevenueSummary(),
|
||||
'expenseSummary' => $this->service->getExpenseSummary(),
|
||||
'orderStats' => $this->service->getOrderStats(),
|
||||
'attendance' => $this->service->getAttendanceStats($user),
|
||||
'revenueSummary' => $this->service->getRevenueSummary($user),
|
||||
'expenseSummary' => $this->service->getExpenseSummary($user),
|
||||
'orderStats' => $this->service->getOrderStats($user),
|
||||
'todayAttendance' => $this->service->getTodayAttendance($user),
|
||||
'isOnLeave' => $this->service->isOnLeave($user),
|
||||
'canCheckIn' => $user->employee !== null,
|
||||
|
||||
@ -69,6 +69,12 @@ public function store(array $data): LeaveRequest
|
||||
|
||||
public function update(LeaveRequest $leaveRequest, array $data): LeaveRequest
|
||||
{
|
||||
if (! $this->canModify($leaveRequest)) {
|
||||
throw ValidationException::withMessages([
|
||||
'employee' => 'Anda tidak memiliki akses untuk mengubah cuti ini.',
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($leaveRequest, $data) {
|
||||
$startDate = new Carbon($data['start_date']);
|
||||
$endDate = new Carbon($data['end_date']);
|
||||
@ -86,6 +92,12 @@ public function update(LeaveRequest $leaveRequest, array $data): LeaveRequest
|
||||
|
||||
public function destroy(LeaveRequest $leaveRequest): bool
|
||||
{
|
||||
if (! $this->canModify($leaveRequest)) {
|
||||
throw ValidationException::withMessages([
|
||||
'employee' => 'Anda tidak memiliki akses untuk menghapus cuti ini.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $leaveRequest->delete();
|
||||
}
|
||||
|
||||
@ -130,4 +142,13 @@ public function reject(LeaveRequest $leaveRequest): LeaveRequest
|
||||
|
||||
return $leaveRequest;
|
||||
}
|
||||
|
||||
private function canModify(LeaveRequest $leaveRequest): bool
|
||||
{
|
||||
if (self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $leaveRequest->employee->user_id === auth()->id();
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ public function __construct(
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?User $user = null): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Order::query()
|
||||
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'tiktok_order_id', 'shopee_order_id', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
||||
@ -53,6 +53,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
'orderItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
'orderItems.productVariant.product:id,name',
|
||||
])
|
||||
->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id))
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->whereHas('orderItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||
->orWhere('order_number', 'like', "%{$search}%")
|
||||
@ -103,14 +104,15 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function getSummary(array $filters = []): array
|
||||
public function getSummary(array $filters = [], ?User $user = null): array
|
||||
{
|
||||
$query = Order::query()
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(subtotal), 0) as total_subtotal')
|
||||
->selectRaw('COALESCE(SUM(discount) + SUM(COALESCE(nego_price, 0)), 0) as total_discount')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
||||
->selectRaw('COALESCE(SUM(discount), 0) as total_discount')
|
||||
->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction')
|
||||
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
||||
->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id))
|
||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
||||
->when($filters['channel'] ?? null, fn ($q, $channel) => $q->where('channel', $channel))
|
||||
->when($filters['payment_type'] ?? null, fn ($q, $paymentType) => $q->where('payment_type', $paymentType))
|
||||
@ -123,9 +125,9 @@ public function getSummary(array $filters = []): array
|
||||
|
||||
return [
|
||||
'total_orders' => $query->total_orders,
|
||||
'total_subtotal' => $query->total_subtotal,
|
||||
'total_discount' => $query->total_discount,
|
||||
'total_amount' => $query->total_amount,
|
||||
'total_discount' => $query->total_discount,
|
||||
'total_deduction' => $query->total_deduction,
|
||||
'net_total' => $query->total_amount - $query->total_cogs,
|
||||
];
|
||||
}
|
||||
@ -397,4 +399,12 @@ private function generateOrderNumber(): string
|
||||
|
||||
return $prefix.$date.str_pad($sequence, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function isMarketingUser(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole([
|
||||
Role::MARKETING_OFFLINE->value,
|
||||
Role::MARKETING_ONLINE->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentType;
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\Employee;
|
||||
@ -20,15 +21,65 @@
|
||||
use App\Models\RestockItem;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AnalysisService
|
||||
{
|
||||
public function getAttendanceStats(?string $startDate, ?string $endDate): array
|
||||
public function getAttendanceStats(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth();
|
||||
$end = $endDate ? Carbon::parse($endDate) : Carbon::now();
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$employee = $user->employee;
|
||||
|
||||
if (! $employee) {
|
||||
return [
|
||||
'total_employees' => 0,
|
||||
'present' => 0,
|
||||
'absent' => 0,
|
||||
'on_leave' => 0,
|
||||
'percentage' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$isWithinRange = $employee->join_date <= $end
|
||||
&& (is_null($employee->resign_date) || $employee->resign_date >= $start);
|
||||
|
||||
if (! $isWithinRange) {
|
||||
return [
|
||||
'total_employees' => 0,
|
||||
'present' => 0,
|
||||
'absent' => 0,
|
||||
'on_leave' => 0,
|
||||
'percentage' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$present = Attendance::where('employee_id', $employee->id)
|
||||
->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()])
|
||||
->distinct('employee_id')
|
||||
->count('employee_id');
|
||||
|
||||
$onLeave = LeaveRequest::approved()
|
||||
->where('start_date', '<=', $end)
|
||||
->where('end_date', '>=', $start)
|
||||
->where('employee_id', $employee->id)
|
||||
->count();
|
||||
|
||||
$workingDays = $this->countWorkingDays($start, $end);
|
||||
$absent = max(0, $workingDays - $present - $onLeave);
|
||||
|
||||
return [
|
||||
'total_employees' => 1,
|
||||
'present' => $present,
|
||||
'absent' => $absent,
|
||||
'on_leave' => $onLeave,
|
||||
'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
$employees = Employee::whereHas('user', fn ($q) => $q
|
||||
->where('is_active', true)
|
||||
->whereHas('roles', fn ($r) => $r
|
||||
@ -83,7 +134,7 @@ public function getMyAttendance(User $user, ?string $startDate, ?string $endDate
|
||||
$current = $start->copy();
|
||||
|
||||
while ($current->lte($end)) {
|
||||
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
||||
if ($current->dayOfWeek !== Carbon::SUNDAY) {
|
||||
$workingDays++;
|
||||
}
|
||||
$current->addDay();
|
||||
@ -189,11 +240,15 @@ public function getProductStock(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getRevenueSummary(?string $startDate, ?string $endDate): array
|
||||
public function getRevenueSummary(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$stats = (clone $query)
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||
@ -213,11 +268,15 @@ public function getRevenueSummary(?string $startDate, ?string $endDate): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getMonthlyRevenue(?string $startDate, ?string $endDate): array
|
||||
public function getMonthlyRevenue(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$monthly = (clone $query)
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||
@ -240,11 +299,15 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate): array
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
|
||||
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array
|
||||
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$monthly = (clone $query)
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END), 0) as store")
|
||||
@ -263,11 +326,15 @@ public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate)
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
|
||||
public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array
|
||||
public function getRevenueByPaymentType(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$data = (clone $query)
|
||||
->select('payment_type')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||
@ -282,8 +349,33 @@ public function getRevenueByPaymentType(?string $startDate, ?string $endDate): a
|
||||
return $data->toArray();
|
||||
}
|
||||
|
||||
public function getExpenseSummary(?string $startDate, ?string $endDate): array
|
||||
public function getExpenseSummary(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$employee = $user->employee;
|
||||
|
||||
$expenseQuery = Expense::where('created_by_id', $user->id);
|
||||
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
|
||||
|
||||
$advanceQuery = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
|
||||
if ($employee) {
|
||||
$advanceQuery->where('employee_id', $employee->id);
|
||||
} else {
|
||||
$advanceQuery->whereRaw('0 = 1');
|
||||
}
|
||||
$this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at');
|
||||
|
||||
$expenseTotal = (clone $expenseQuery)->sum('amount');
|
||||
$advanceTotal = (clone $advanceQuery)->sum('amount');
|
||||
|
||||
return [
|
||||
'total' => (int) ($expenseTotal + $advanceTotal),
|
||||
'purchase_total' => 0,
|
||||
'expense_total' => (int) $expenseTotal,
|
||||
'advance_total' => (int) $advanceTotal,
|
||||
];
|
||||
}
|
||||
|
||||
$expenseQuery = Expense::query();
|
||||
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
|
||||
|
||||
@ -305,8 +397,54 @@ public function getExpenseSummary(?string $startDate, ?string $endDate): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||
public function getMonthlyExpense(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$employee = $user->employee;
|
||||
|
||||
$expenseMonthly = Expense::where('created_by_id', $user->id);
|
||||
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
|
||||
|
||||
$expenseByMonth = (clone $expenseMonthly)->toBase()
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as expense')
|
||||
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||
->get()
|
||||
->keyBy('month');
|
||||
|
||||
$advanceMonthly = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
|
||||
if ($employee) {
|
||||
$advanceMonthly->where('employee_id', $employee->id);
|
||||
} else {
|
||||
$advanceMonthly->whereRaw('0 = 1');
|
||||
}
|
||||
$this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at');
|
||||
|
||||
$advanceByMonth = (clone $advanceMonthly)->toBase()
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as advance')
|
||||
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||
->get()
|
||||
->keyBy('month');
|
||||
|
||||
$allMonths = [];
|
||||
foreach ([$expenseByMonth, $advanceByMonth] as $data) {
|
||||
foreach ($data as $month => $row) {
|
||||
if (! array_key_exists($month, $allMonths)) {
|
||||
$allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allMonths as $month => &$row) {
|
||||
$row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0);
|
||||
$row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0);
|
||||
$row['total'] = $row['expense'] + $row['advance'];
|
||||
}
|
||||
|
||||
return array_values($allMonths);
|
||||
}
|
||||
|
||||
$expenseMonthly = Expense::query();
|
||||
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
|
||||
|
||||
@ -356,11 +494,15 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||
return array_values($allMonths);
|
||||
}
|
||||
|
||||
public function getBusyHours(?string $startDate, ?string $endDate): array
|
||||
public function getBusyHours(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$hours = range(0, 23);
|
||||
$hourCounts = (clone $query)
|
||||
->selectRaw('HOUR(created_at) as hour')
|
||||
@ -377,11 +519,15 @@ public function getBusyHours(?string $startDate, ?string $endDate): array
|
||||
}, $hours);
|
||||
}
|
||||
|
||||
public function getProfitMetrics(?string $startDate, ?string $endDate): array
|
||||
public function getProfitMetrics(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$stats = (clone $query)
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||
@ -427,11 +573,15 @@ public function getTopSuppliers(?string $startDate, ?string $endDate): array
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getTopCustomers(?string $startDate, ?string $endDate): array
|
||||
public function getTopCustomers(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id');
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
return (clone $query)->toBase()
|
||||
->join('customers', 'orders.customer_id', '=', 'customers.id')
|
||||
->select('customers.name')
|
||||
@ -444,11 +594,15 @@ public function getTopCustomers(?string $startDate, ?string $endDate): array
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getTopProducts(?string $startDate, ?string $endDate): array
|
||||
public function getTopProducts(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
return (clone $query)->toBase()
|
||||
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
|
||||
@ -463,11 +617,15 @@ public function getTopProducts(?string $startDate, ?string $endDate): array
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getRevenueTrend(?string $startDate, ?string $endDate): array
|
||||
public function getRevenueTrend(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
return (clone $query)
|
||||
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||
->selectRaw('DATE(orders.created_at) as date')
|
||||
@ -482,12 +640,16 @@ public function getRevenueTrend(?string $startDate, ?string $endDate): array
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getMarketingSales(?string $startDate, ?string $endDate): array
|
||||
public function getMarketingSales(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED)
|
||||
->whereNotNull('orders.marketing_id');
|
||||
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$query->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$orders = (clone $query)
|
||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||
@ -521,11 +683,15 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function getOrderStats(?string $startDate, ?string $endDate): array
|
||||
public function getOrderStats(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||
{
|
||||
$baseQuery = Order::query();
|
||||
$this->applyDateFilter($baseQuery, $startDate, $endDate, 'orders.created_at');
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$baseQuery->where('orders.marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$byChannel = collect(OrderChannel::values())->map(function ($channel) use ($baseQuery) {
|
||||
$count = (clone $baseQuery)->where('channel', $channel)->count();
|
||||
$label = OrderChannel::from($channel)->label();
|
||||
@ -592,4 +758,36 @@ private function applyDateFilter($query, ?string $startDate, ?string $endDate, s
|
||||
$query->whereDate($dateColumn, '<=', $endDate);
|
||||
}
|
||||
}
|
||||
|
||||
private function isMarketingUser(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole([
|
||||
Role::MARKETING_OFFLINE->value,
|
||||
Role::MARKETING_ONLINE->value,
|
||||
]);
|
||||
}
|
||||
|
||||
private function applyMarketingFilter(Builder $query, User $user, string $column = 'marketing_id'): Builder
|
||||
{
|
||||
if ($this->isMarketingUser($user)) {
|
||||
$query->where($column, $user->id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function countWorkingDays(Carbon $start, Carbon $end): int
|
||||
{
|
||||
$workingDays = 0;
|
||||
$current = $start->copy();
|
||||
|
||||
while ($current->lte($end)) {
|
||||
if ($current->dayOfWeek !== Carbon::SUNDAY) {
|
||||
$workingDays++;
|
||||
}
|
||||
$current->addDay();
|
||||
}
|
||||
|
||||
return $workingDays;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentType;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Models\EmployeeAdvance;
|
||||
@ -13,13 +14,49 @@
|
||||
use App\Models\Order;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
public function getAttendanceStats(): array
|
||||
public function getAttendanceStats(?User $user = null): array
|
||||
{
|
||||
$today = Carbon::now()->toDateString();
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$employee = $user->employee;
|
||||
|
||||
if (! $employee) {
|
||||
return [
|
||||
'total_employees' => 0,
|
||||
'present' => 0,
|
||||
'absent' => 0,
|
||||
'on_leave' => 0,
|
||||
'percentage' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$present = Attendance::where('employee_id', $employee->id)
|
||||
->where('attendance_date', $today)
|
||||
->count();
|
||||
|
||||
$onLeave = LeaveRequest::approved()
|
||||
->where('employee_id', $employee->id)
|
||||
->where('start_date', '<=', $today)
|
||||
->where('end_date', '>=', $today)
|
||||
->count();
|
||||
|
||||
$isWorking = ! in_array(Carbon::now()->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]);
|
||||
$absent = $isWorking && $present === 0 && $onLeave === 0 ? 1 : 0;
|
||||
|
||||
return [
|
||||
'total_employees' => 1,
|
||||
'present' => $present,
|
||||
'absent' => $absent,
|
||||
'on_leave' => $onLeave,
|
||||
'percentage' => $present > 0 ? 100 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
$totalEmployees = Employee::whereHas(
|
||||
'user',
|
||||
fn ($q) => $q
|
||||
@ -80,12 +117,16 @@ public function getAttendanceStats(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getRevenueSummary(): array
|
||||
public function getRevenueSummary(?User $user = null): array
|
||||
{
|
||||
$today = Carbon::now()->toDateString();
|
||||
$baseQuery = Order::where('status', OrderStatus::COMPLETED)
|
||||
->whereDate('created_at', $today);
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$baseQuery->where('marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$stats = (clone $baseQuery)
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||
@ -104,10 +145,38 @@ public function getRevenueSummary(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getExpenseSummary(): array
|
||||
public function getExpenseSummary(?User $user = null): array
|
||||
{
|
||||
$today = Carbon::now()->toDateString();
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$employee = $user->employee;
|
||||
|
||||
$expenseTotal = Expense::whereDate('created_at', $today)
|
||||
->where('created_by_id', $user->id)
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
$advanceQuery = EmployeeAdvance::whereDate('created_at', $today)
|
||||
->disbursed();
|
||||
|
||||
if ($employee) {
|
||||
$advanceQuery->where('employee_id', $employee->id);
|
||||
} else {
|
||||
$advanceQuery->whereRaw('0 = 1');
|
||||
}
|
||||
|
||||
$advanceTotal = $advanceQuery
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total' => (int) $expenseTotal->total + (int) $advanceTotal->total,
|
||||
'expense_total' => (int) $expenseTotal->total,
|
||||
'advance_total' => (int) $advanceTotal->total,
|
||||
];
|
||||
}
|
||||
|
||||
$expenseTotal = Expense::whereDate('created_at', $today)
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
@ -124,11 +193,15 @@ public function getExpenseSummary(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getOrderStats(): array
|
||||
public function getOrderStats(?User $user = null): array
|
||||
{
|
||||
$today = Carbon::now()->toDateString();
|
||||
$baseQuery = Order::whereDate('created_at', $today);
|
||||
|
||||
if ($user && $this->isMarketingUser($user)) {
|
||||
$baseQuery->where('marketing_id', $user->id);
|
||||
}
|
||||
|
||||
$byChannel = collect(OrderChannel::values())->map(function ($channel) use ($baseQuery) {
|
||||
$count = (clone $baseQuery)->where('channel', $channel)->count();
|
||||
$label = OrderChannel::from($channel)->label();
|
||||
@ -231,4 +304,21 @@ public function isOnLeave(User $user): bool
|
||||
->where('end_date', '>=', now()->toDateString())
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function isMarketingUser(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole([
|
||||
Role::MARKETING_OFFLINE->value,
|
||||
Role::MARKETING_ONLINE->value,
|
||||
]);
|
||||
}
|
||||
|
||||
private function applyMarketingFilter(Builder $query, User $user, string $column = 'marketing_id'): Builder
|
||||
{
|
||||
if ($this->isMarketingUser($user)) {
|
||||
$query->where($column, $user->id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +92,6 @@ public function run(): void
|
||||
'analysis.profit_margin',
|
||||
'analysis.top_customers',
|
||||
'analysis.top_products',
|
||||
'analysis.top_suppliers',
|
||||
'analysis.marketing_sales',
|
||||
|
||||
'stok_opnames.view',
|
||||
@ -381,7 +380,6 @@ public function run(): void
|
||||
'analysis.profit_margin',
|
||||
'analysis.top_customers',
|
||||
'analysis.top_products',
|
||||
'analysis.top_suppliers',
|
||||
'analysis.marketing_sales',
|
||||
|
||||
'employees.view',
|
||||
|
||||
@ -105,7 +105,7 @@ const hrItems: NavMenuItem[] = [
|
||||
const sistemItems: NavMenuItem[] = [
|
||||
{ title: 'Pengaturan', href: '/admin/settings', icon: Settings, permission: ['settings.view_system', 'settings.view_homepage', 'settings.view_social_media', 'settings.view_marketplace', 'settings.view_hr'] },
|
||||
{ title: 'Role & Permission', href: rolesIndex.url(), icon: Shield, permission: 'roles.view' },
|
||||
{ title: 'Log Aktivitas', href: '#', icon: Activity, permission: 'activity_logs.view' },
|
||||
// { title: 'Log Aktivitas', href: '#', icon: Activity, permission: 'activity_logs.view' },
|
||||
];
|
||||
|
||||
function MenuGroup({ label, items }: { label: string; items: NavMenuItem[] }) {
|
||||
|
||||
@ -13,6 +13,7 @@ export type LeaveRequest = {
|
||||
status: 'pending' | 'approved' | 'rejected' | 'cancelled';
|
||||
created_at: string;
|
||||
employee: {
|
||||
user_id: number;
|
||||
user: {
|
||||
user_profile: {
|
||||
full_name: string;
|
||||
@ -56,12 +57,13 @@ type CreateColumnsParams = {
|
||||
handleApprove: (leaveRequest: LeaveRequest) => void;
|
||||
handleReject: (leaveRequest: LeaveRequest) => void;
|
||||
can: (permission: string) => boolean;
|
||||
currentUserId: number;
|
||||
};
|
||||
|
||||
export function createLeaveRequestColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<LeaveRequest>[] {
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can, currentUserId } =
|
||||
params;
|
||||
|
||||
const columns: ColumnDef<LeaveRequest>[] = [
|
||||
@ -157,7 +159,8 @@ export function createLeaveRequestColumns(
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show:
|
||||
can('leave_requests.update') &&
|
||||
leaveRequest.status === 'pending',
|
||||
leaveRequest.status === 'pending' &&
|
||||
leaveRequest.employee?.user_id === currentUserId,
|
||||
onClick: () => handleEdit(leaveRequest),
|
||||
},
|
||||
{
|
||||
@ -167,7 +170,8 @@ export function createLeaveRequestColumns(
|
||||
),
|
||||
show:
|
||||
can('leave_requests.delete') &&
|
||||
leaveRequest.status === 'pending',
|
||||
leaveRequest.status === 'pending' &&
|
||||
leaveRequest.employee?.user_id === currentUserId,
|
||||
onClick: () => handleDeleteClick(leaveRequest),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -50,6 +50,7 @@ type Props = {
|
||||
filterOptions: {
|
||||
statusOptions: StatusOption[];
|
||||
};
|
||||
currentUserId: number;
|
||||
};
|
||||
|
||||
function toLocalDateString(date: Date): string {
|
||||
@ -59,7 +60,7 @@ function toLocalDateString(date: Date): string {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) {
|
||||
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions, currentUserId }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
||||
@ -151,6 +152,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
||||
handleApprove: (leaveRequest) => setApproving(leaveRequest),
|
||||
handleReject: (leaveRequest) => setRejecting(leaveRequest),
|
||||
can,
|
||||
currentUserId,
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
|
||||
@ -67,9 +67,9 @@ type Props = {
|
||||
};
|
||||
summary: {
|
||||
total_orders: number;
|
||||
total_subtotal: number;
|
||||
total_discount: number;
|
||||
total_amount: number;
|
||||
total_discount: number;
|
||||
total_deduction: number;
|
||||
net_total: number;
|
||||
};
|
||||
filters: {
|
||||
|
||||
@ -122,6 +122,15 @@ export function TransactionCardRow({
|
||||
?.full_name ?? '-'}
|
||||
</span>
|
||||
</span>
|
||||
{transaction.marketing && (
|
||||
<span className="text-muted-foreground">
|
||||
Marketing:{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{transaction.marketing.user_profile
|
||||
?.full_name ?? '-'}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{transaction.customer && (
|
||||
<span className="text-muted-foreground">
|
||||
Pelanggan:{' '}
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
Banknote,
|
||||
CircleDollarSign,
|
||||
FileText,
|
||||
Minus,
|
||||
Percent,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
@ -10,9 +11,9 @@ import { formatCurrency } from '@/lib/utils';
|
||||
|
||||
type Summary = {
|
||||
total_orders: number;
|
||||
total_subtotal: number;
|
||||
total_discount: number;
|
||||
total_amount: number;
|
||||
total_discount: number;
|
||||
total_deduction: number;
|
||||
net_total: number;
|
||||
};
|
||||
|
||||
@ -30,8 +31,8 @@ const summaryItems = [
|
||||
format: (value: number) => value.toLocaleString('id-ID'),
|
||||
},
|
||||
{
|
||||
key: 'total_subtotal',
|
||||
label: 'Subtotal',
|
||||
key: 'total_amount',
|
||||
label: 'Total',
|
||||
icon: Banknote,
|
||||
color: 'bg-emerald-100',
|
||||
iconColor: 'text-emerald-600',
|
||||
@ -46,11 +47,11 @@ const summaryItems = [
|
||||
format: formatCurrency,
|
||||
},
|
||||
{
|
||||
key: 'total_amount',
|
||||
label: 'Total Uang',
|
||||
icon: CircleDollarSign,
|
||||
color: 'bg-purple-100',
|
||||
iconColor: 'text-purple-600',
|
||||
key: 'total_deduction',
|
||||
label: 'Potongan',
|
||||
icon: Minus,
|
||||
color: 'bg-orange-100',
|
||||
iconColor: 'text-orange-600',
|
||||
format: formatCurrency,
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user