dstpabuaran.com/app/Services/Admin/HR/AttendanceService.php

304 lines
12 KiB
PHP

<?php
namespace App\Services\Admin\HR;
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 Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class AttendanceService
{
use RegistersMedia;
public function __construct(
private S3PresignedService $s3Service,
) {}
public function getAll(): Collection
{
return Attendance::with(['employee.user.userProfile', 'media'])
->latest('attendance_date')
->get()
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
}
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 getToday(): ?array
{
return $this->getByDate(now()->toDateString());
}
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 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 getAllEmployees(): Collection
{
return Employee::with(['user.userProfile', 'user.roles'])
->whereHas('user', fn ($q) => $q->where('is_active', true))
->get()
->map(fn (Employee $employee) => [
'id' => $employee->id,
'name' => $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);
}
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', fn ($q) => $q->where('is_active', true))->count();
$presentCount = Attendance::whereYear('attendance_date', $year)
->whereMonth('attendance_date', $month)
->where('attendance_date', '<=', $statEnd->toDateString())
->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
->count();
$leaveRequests = LeaveRequest::approved()
->where('start_date', '<=', $statEnd)
->where('end_date', '>=', $startOfMonth)
->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
->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,
];
}
public function checkIn(array $data): Attendance
{
$employee = auth()->user()->employee;
if (! $employee) {
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
}
$today = now()->toDateString();
$existing = Attendance::where('employee_id', $employee->id)
->where('attendance_date', $today)
->first();
if ($existing) {
throw new \Exception('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'], 'check-in', 'attendances');
}
NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur'],
title: 'Presensi Masuk',
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
url: route('admin.hr.attendances.index'),
);
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'], 'check-out', 'attendances');
}
NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur'],
title: 'Presensi Pulang',
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
url: route('admin.hr.attendances.index'),
);
return $attendance;
}
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('check-in');
$checkOutMedia = $attendance->getFirstMedia('check-out');
$toArray['check_in_photo'] = $checkInMedia
? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkInMedia->file_name))
: null;
$toArray['check_out_photo'] = $checkOutMedia
? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkOutMedia->file_name))
: null;
return $toArray;
}
}