357 lines
14 KiB
PHP
357 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\HR;
|
|
|
|
use App\Concerns\HasRoleChecks;
|
|
use App\Enums\Role;
|
|
use App\Models\Attendance;
|
|
use App\Models\Employee;
|
|
use App\Models\LeaveRequest;
|
|
use App\Models\User;
|
|
use App\Services\Concerns\RegistersMedia;
|
|
use App\Services\NotificationService;
|
|
use App\Services\S3PresignedService;
|
|
use App\Settings\HRSettings;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AttendanceService
|
|
{
|
|
use HasRoleChecks, RegistersMedia;
|
|
|
|
public function __construct(
|
|
private S3PresignedService $s3Service,
|
|
private EmployeeService $employeeService,
|
|
) {}
|
|
|
|
public function getIndexData(int $year, int $month): array
|
|
{
|
|
$user = auth()->user();
|
|
$isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::ADMIN_TOKO, Role::DIREKTUR]);
|
|
$hrSettings = app(HRSettings::class);
|
|
|
|
$employeeId = $isAdmin ? null : $user->employee?->id;
|
|
|
|
return [
|
|
'attendances' => $this->getByMonth($year, $month, $employeeId),
|
|
'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
|
|
'employees' => $isAdmin ? $this->getEmployeesWithAttendancePermission() : [],
|
|
'todayAttendance' => $isAdmin ? null : $this->getToday(),
|
|
'currentYear' => $year,
|
|
'currentMonth' => $month,
|
|
'monthStats' => $this->getMonthStats($year, $month, $employeeId),
|
|
'hrSettings' => [
|
|
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
|
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
|
],
|
|
'isOnLeave' => $isAdmin ? false : $this->isOnLeave($user),
|
|
'canCheckIn' => $isAdmin ? false : $user->employee !== null,
|
|
'isAdmin' => $isAdmin,
|
|
];
|
|
}
|
|
|
|
public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
|
{
|
|
return Attendance::with(['employee.user.userProfile', 'media'])
|
|
->whereYear('attendance_date', $year)
|
|
->whereMonth('attendance_date', $month)
|
|
->when($employeeId, fn($q) => $q->where('employee_id', $employeeId))
|
|
->get()
|
|
->map(fn(Attendance $attendance) => $this->formatAttendance($attendance));
|
|
}
|
|
|
|
public function getByDate(string $date): ?array
|
|
{
|
|
$attendance = Attendance::with(['employee.user.userProfile', 'media'])
|
|
->where('attendance_date', $date)
|
|
->where('employee_id', auth()->user()->employee?->id)
|
|
->first();
|
|
|
|
return $attendance ? $this->formatAttendance($attendance) : null;
|
|
}
|
|
|
|
public function getByMonthData(int $year, int $month): array
|
|
{
|
|
$employeeId = null;
|
|
|
|
return [
|
|
'attendances' => $this->getByMonth($year, $month, $employeeId),
|
|
'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
|
|
'employees' => $this->getEmployeesWithAttendancePermission(),
|
|
'monthStats' => $this->getMonthStats($year, $month, $employeeId),
|
|
];
|
|
}
|
|
|
|
public function getToday(): ?array
|
|
{
|
|
return $this->getByDate(now()->toDateString());
|
|
}
|
|
|
|
public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
|
{
|
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
|
|
|
return LeaveRequest::approved()
|
|
->with('employee.user.userProfile')
|
|
->where('start_date', '<=', $endOfMonth)
|
|
->where('end_date', '>=', $startOfMonth)
|
|
->when($employeeId, fn($q) => $q->where('employee_id', $employeeId))
|
|
->get()
|
|
->map(fn(LeaveRequest $leave) => [
|
|
'id' => $leave->id,
|
|
'employee_id' => $leave->employee_id,
|
|
'start_date' => $leave->start_date->toDateString(),
|
|
'end_date' => $leave->end_date->toDateString(),
|
|
'total_days' => $leave->total_days,
|
|
'status' => $leave->status->value,
|
|
'employee_name' => $leave->employee?->user?->userProfile?->full_name ?? '-',
|
|
]);
|
|
}
|
|
|
|
public function getMonthStats(int $year, int $month, ?int $employeeId = null): array
|
|
{
|
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
|
$today = Carbon::today();
|
|
$statEnd = $endOfMonth->lte($today) ? $endOfMonth : $today;
|
|
|
|
$workingDays = 0;
|
|
$current = $startOfMonth->copy();
|
|
while ($current->lte($statEnd)) {
|
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
|
$workingDays++;
|
|
}
|
|
$current->addDay();
|
|
}
|
|
|
|
if ($employeeId) {
|
|
return $this->getMonthStatsForEmployee($year, $month, $startOfMonth, $statEnd, $workingDays, $employeeId);
|
|
}
|
|
|
|
return $this->getMonthStatsForAll($year, $month, $startOfMonth, $statEnd, $workingDays);
|
|
}
|
|
|
|
public function isOnLeave(User $user): bool
|
|
{
|
|
$employee = $user->employee;
|
|
|
|
if (! $employee) {
|
|
return false;
|
|
}
|
|
|
|
return LeaveRequest::approved()
|
|
->where('employee_id', $employee->id)
|
|
->where('start_date', '<=', now()->toDateString())
|
|
->where('end_date', '>=', now()->toDateString())
|
|
->exists();
|
|
}
|
|
|
|
public function checkIn(array $data): Attendance
|
|
{
|
|
$employee = auth()->user()->employee;
|
|
|
|
if (! $employee) {
|
|
throw ValidationException::withMessages([
|
|
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
|
|
]);
|
|
}
|
|
|
|
$today = now()->toDateString();
|
|
|
|
$existing = Attendance::where('employee_id', $employee->id)
|
|
->where('attendance_date', $today)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
throw ValidationException::withMessages([
|
|
'attendance' => 'Anda sudah melakukan presensi hari ini.',
|
|
]);
|
|
}
|
|
|
|
$attendance = Attendance::create([
|
|
'employee_id' => $employee->id,
|
|
'attendance_date' => $today,
|
|
'check_in_at' => now(),
|
|
'check_in_latitude' => $data['latitude'],
|
|
'check_in_longitude' => $data['longitude'],
|
|
]);
|
|
|
|
if (! empty($data['photo'])) {
|
|
$this->registerMediaFromBase64($attendance, $data['photo'], 'checkin', 'attendances');
|
|
}
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
|
title: 'Presensi Masuk',
|
|
body: 'Presensi masuk oleh ' . auth()->user()->full_name . '.',
|
|
url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]),
|
|
);
|
|
|
|
return $attendance;
|
|
}
|
|
|
|
public function checkOut(Attendance $attendance, array $data): Attendance
|
|
{
|
|
$attendance->update([
|
|
'check_out_at' => now(),
|
|
'check_out_latitude' => $data['latitude'],
|
|
'check_out_longitude' => $data['longitude'],
|
|
]);
|
|
|
|
if (! empty($data['photo'])) {
|
|
$this->registerMediaFromBase64($attendance, $data['photo'], 'checkout', 'attendances');
|
|
}
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
|
title: 'Presensi Pulang',
|
|
body: 'Presensi pulang oleh ' . auth()->user()->full_name . '.',
|
|
url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]),
|
|
);
|
|
|
|
return $attendance;
|
|
}
|
|
|
|
private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays, int $employeeId): array
|
|
{
|
|
$attendanceQuery = Attendance::whereYear('attendance_date', $year)
|
|
->whereMonth('attendance_date', $month)
|
|
->where('attendance_date', '<=', $statEnd->toDateString())
|
|
->where('employee_id', $employeeId);
|
|
|
|
$attendanceDates = $attendanceQuery->pluck('attendance_date')
|
|
->map(fn($d) => Carbon::parse($d)->toDateString())
|
|
->filter(fn($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]))
|
|
->unique()
|
|
->values();
|
|
$attendanceCount = $attendanceDates->count();
|
|
|
|
$leaveQuery = LeaveRequest::approved()
|
|
->where('start_date', '<=', $statEnd)
|
|
->where('end_date', '>=', $startOfMonth)
|
|
->where('employee_id', $employeeId);
|
|
|
|
$leaveRequests = $leaveQuery->get();
|
|
$leaveCount = $leaveRequests->count();
|
|
|
|
$leaveDates = collect();
|
|
$leaveRequests->each(function ($leave) use (&$leaveDates, $startOfMonth, $statEnd) {
|
|
$leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
|
|
$leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
|
|
$current = $leaveStart->copy();
|
|
while ($current->lte($leaveEnd)) {
|
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
|
$leaveDates->push($current->toDateString());
|
|
}
|
|
$current->addDay();
|
|
}
|
|
});
|
|
$leaveDates = $leaveDates->unique()->values();
|
|
|
|
$coveredDates = $attendanceDates->merge($leaveDates)->unique()->count();
|
|
$absent = max(0, $workingDays - $coveredDates);
|
|
|
|
return [
|
|
'working_days' => $workingDays,
|
|
'present' => $attendanceCount,
|
|
'absent' => $absent,
|
|
'leave' => $leaveCount,
|
|
];
|
|
}
|
|
|
|
private function getMonthStatsForAll(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays): array
|
|
{
|
|
$totalEmployees = Employee::whereHas('user', function ($q) {
|
|
$q->active()
|
|
->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view')));
|
|
})->count();
|
|
|
|
$presentCount = Attendance::whereYear('attendance_date', $year)
|
|
->whereMonth('attendance_date', $month)
|
|
->where('attendance_date', '<=', $statEnd->toDateString())
|
|
->whereHas('employee', function ($q) {
|
|
$q->whereHas('user', function ($uq) {
|
|
$uq->active()
|
|
->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view')));
|
|
});
|
|
})
|
|
->count();
|
|
|
|
$leaveRequests = LeaveRequest::approved()
|
|
->where('start_date', '<=', $statEnd)
|
|
->where('end_date', '>=', $startOfMonth)
|
|
->whereHas('employee', function ($q) {
|
|
$q->whereHas('user', function ($uq) {
|
|
$uq->active()
|
|
->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view')));
|
|
});
|
|
})
|
|
->get();
|
|
|
|
$leaveDays = 0;
|
|
$leaveRequests->each(function ($leave) use (&$leaveDays, $startOfMonth, $statEnd) {
|
|
$leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
|
|
$leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
|
|
$current = $leaveStart->copy();
|
|
while ($current->lte($leaveEnd)) {
|
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
|
$leaveDays++;
|
|
}
|
|
$current->addDay();
|
|
}
|
|
});
|
|
|
|
$absent = max(0, ($totalEmployees * $workingDays) - $presentCount - $leaveDays);
|
|
|
|
return [
|
|
'working_days' => $workingDays,
|
|
'present' => $presentCount,
|
|
'absent' => $absent,
|
|
'leave' => $leaveDays,
|
|
'total_employees' => $totalEmployees,
|
|
];
|
|
}
|
|
|
|
private function formatAttendance(Attendance $attendance): array
|
|
{
|
|
$toArray = $attendance->toArray();
|
|
$toArray['employee_name'] = $attendance->employee?->user?->userProfile?->full_name ?? '-';
|
|
|
|
if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) {
|
|
$checkIn = Carbon::parse($attendance->check_in_at);
|
|
$end = $checkIn->toDateString() === now()->toDateString()
|
|
? now()
|
|
: $checkIn->copy()->endOfDay();
|
|
$toArray['work_duration_minutes'] = $checkIn->diffInMinutes($end);
|
|
}
|
|
|
|
$checkInMedia = $attendance->getFirstMedia('checkin');
|
|
$checkOutMedia = $attendance->getFirstMedia('checkout');
|
|
|
|
$toArray['check_in_photo'] = $checkInMedia
|
|
? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn() => $this->s3Service->getTemporaryUrl($checkInMedia->getPath()))
|
|
: null;
|
|
|
|
$toArray['check_out_photo'] = $checkOutMedia
|
|
? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn() => $this->s3Service->getTemporaryUrl($checkOutMedia->getPath()))
|
|
: null;
|
|
|
|
return $toArray;
|
|
}
|
|
|
|
private function getEmployeesWithAttendancePermission(): Collection
|
|
{
|
|
return Employee::with(['user.userProfile', 'user.roles'])
|
|
->whereHas('user', fn ($q) => $q->where('is_active', true)
|
|
->whereHas('roles', fn ($rq) => $rq->whereHas('permissions', fn ($pq) => $pq->where('name', 'attendances.view'))))
|
|
->get()
|
|
->map(fn (Employee $employee) => [
|
|
'id' => $employee->id,
|
|
'name' => $employee->user?->userProfile?->full_name ?? '-',
|
|
]);
|
|
}
|
|
}
|