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:
Yoga Pangestu 2026-08-13 21:08:39 +07:00
parent 5e07a1b910
commit 8053ea2db4
15 changed files with 398 additions and 62 deletions

View File

@ -29,6 +29,7 @@ public function index(PaginatedRequest $request): Response
'filterOptions' => [
'statusOptions' => LeaveRequestStatus::toSelect(),
],
'currentUserId' => $request->user()->id,
]);
}

View File

@ -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(),
]);

View File

@ -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' => [

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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,
]);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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[] }) {

View File

@ -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),
},
]}

View File

@ -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 = (

View File

@ -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: {

View File

@ -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:{' '}

View File

@ -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,
},
{