diff --git a/app/Http/Controllers/Admin/HR/LeaveRequestController.php b/app/Http/Controllers/Admin/HR/LeaveRequestController.php index 5abd2b0..2e3136c 100644 --- a/app/Http/Controllers/Admin/HR/LeaveRequestController.php +++ b/app/Http/Controllers/Admin/HR/LeaveRequestController.php @@ -29,6 +29,7 @@ public function index(PaginatedRequest $request): Response 'filterOptions' => [ 'statusOptions' => LeaveRequestStatus::toSelect(), ], + 'currentUserId' => $request->user()->id, ]); } diff --git a/app/Http/Controllers/Admin/Manage/TransactionController.php b/app/Http/Controllers/Admin/Manage/TransactionController.php index 994ffa4..0dfd4a0 100644 --- a/app/Http/Controllers/Admin/Manage/TransactionController.php +++ b/app/Http/Controllers/Admin/Manage/TransactionController.php @@ -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(), ]); diff --git a/app/Http/Controllers/AnalysisController.php b/app/Http/Controllers/AnalysisController.php index d5099a9..5a620ae 100644 --- a/app/Http/Controllers/AnalysisController.php +++ b/app/Http/Controllers/AnalysisController.php @@ -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' => [ diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index c39d3b9..7f27fb8 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -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, diff --git a/app/Services/Admin/HR/LeaveRequestService.php b/app/Services/Admin/HR/LeaveRequestService.php index 69b3e1b..d24658c 100644 --- a/app/Services/Admin/HR/LeaveRequestService.php +++ b/app/Services/Admin/HR/LeaveRequestService.php @@ -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(); + } } diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index 1d18922..847ba12 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -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, + ]); + } } diff --git a/app/Services/AnalysisService.php b/app/Services/AnalysisService.php index 92bf4af..be71919 100644 --- a/app/Services/AnalysisService.php +++ b/app/Services/AnalysisService.php @@ -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; + } } diff --git a/app/Services/DashboardService.php b/app/Services/DashboardService.php index 380574e..c8da7e0 100644 --- a/app/Services/DashboardService.php +++ b/app/Services/DashboardService.php @@ -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; + } } diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 49bb68c..0287206 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -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', diff --git a/resources/js/components/layout/app-sidebar.tsx b/resources/js/components/layout/app-sidebar.tsx index 5374c4c..4ff003d 100644 --- a/resources/js/components/layout/app-sidebar.tsx +++ b/resources/js/components/layout/app-sidebar.tsx @@ -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[] }) { diff --git a/resources/js/pages/admin/hr/leave-request/columns.tsx b/resources/js/pages/admin/hr/leave-request/columns.tsx index fae87b7..2a4b506 100644 --- a/resources/js/pages/admin/hr/leave-request/columns.tsx +++ b/resources/js/pages/admin/hr/leave-request/columns.tsx @@ -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[] { - const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } = + const { handleEdit, handleDeleteClick, handleApprove, handleReject, can, currentUserId } = params; const columns: ColumnDef[] = [ @@ -157,7 +159,8 @@ export function createLeaveRequestColumns( icon: , 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), }, ]} diff --git a/resources/js/pages/admin/hr/leave-request/index.tsx b/resources/js/pages/admin/hr/leave-request/index.tsx index 2b847c7..e247ccf 100644 --- a/resources/js/pages/admin/hr/leave-request/index.tsx +++ b/resources/js/pages/admin/hr/leave-request/index.tsx @@ -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(null); @@ -151,6 +152,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption handleApprove: (leaveRequest) => setApproving(leaveRequest), handleReject: (leaveRequest) => setRejecting(leaveRequest), can, + currentUserId, }); const filterToolbar = ( diff --git a/resources/js/pages/admin/manage/transaction/index.tsx b/resources/js/pages/admin/manage/transaction/index.tsx index 06d3b15..a27217a 100644 --- a/resources/js/pages/admin/manage/transaction/index.tsx +++ b/resources/js/pages/admin/manage/transaction/index.tsx @@ -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: { diff --git a/resources/js/pages/admin/manage/transaction/transaction-card.tsx b/resources/js/pages/admin/manage/transaction/transaction-card.tsx index 1612b18..db4f5fd 100644 --- a/resources/js/pages/admin/manage/transaction/transaction-card.tsx +++ b/resources/js/pages/admin/manage/transaction/transaction-card.tsx @@ -122,6 +122,15 @@ export function TransactionCardRow({ ?.full_name ?? '-'} + {transaction.marketing && ( + + Marketing:{' '} + + {transaction.marketing.user_profile + ?.full_name ?? '-'} + + + )} {transaction.customer && ( Pelanggan:{' '} diff --git a/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx b/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx index 04a50bc..1f04200 100644 --- a/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx +++ b/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx @@ -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, }, {