229 lines
8.0 KiB
PHP
229 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hr;
|
|
|
|
use App\Enums\Permission;
|
|
use App\Enums\Role;
|
|
use App\Models\Attendance;
|
|
use App\Models\Employee;
|
|
use App\Models\LeaveRequest;
|
|
use App\Models\User;
|
|
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
|
use App\Services\Concerns\RunsInTransaction;
|
|
use App\Services\Media\MediaService;
|
|
use App\Services\System\PushNotificationService;
|
|
use Carbon\CarbonInterface;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AttendanceService
|
|
{
|
|
use ResolvesAuthenticatedEmployee, RunsInTransaction;
|
|
|
|
public function __construct(
|
|
private readonly MediaService $mediaService,
|
|
private readonly PushNotificationService $pushNotificationService,
|
|
) {}
|
|
|
|
public function listForCalendar(
|
|
CarbonInterface $start,
|
|
CarbonInterface $end,
|
|
?int $scopedEmployeeId = null,
|
|
bool $hasScopedAccess = true,
|
|
): Collection {
|
|
return Attendance::query()
|
|
->with(['employee.user.profile', 'media'])
|
|
->when(! $hasScopedAccess, function (Builder $query): void {
|
|
$query->whereRaw('1 = 0');
|
|
})
|
|
->when($hasScopedAccess && $scopedEmployeeId !== null, function (Builder $query) use ($scopedEmployeeId): void {
|
|
$query->where('employee_id', $scopedEmployeeId);
|
|
})
|
|
->whereDate('attendance_date', '>=', $start->toDateString())
|
|
->whereDate('attendance_date', '<', $end->toDateString())
|
|
->orderBy('attendance_date')
|
|
->orderBy('check_in_at')
|
|
->get();
|
|
}
|
|
|
|
public function todayAttendanceForEmployee(Employee $employee): ?array
|
|
{
|
|
$attendance = Attendance::query()
|
|
->with('media')
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('attendance_date', today())
|
|
->first();
|
|
|
|
return $attendance?->toArray();
|
|
}
|
|
|
|
public function isOnLeaveToday(Employee $employee): bool
|
|
{
|
|
return LeaveRequest::query()
|
|
->approved()
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('start_date', '<=', today())
|
|
->whereDate('end_date', '>=', today())
|
|
->exists();
|
|
}
|
|
|
|
public function checkIn(array $validated, User $user): void
|
|
{
|
|
$employee = $this->resolveAuthenticatedEmployee($user);
|
|
|
|
$this->runInTransaction(
|
|
function () use ($employee, $validated): void {
|
|
$existing = Attendance::query()
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('attendance_date', today())
|
|
->exists();
|
|
|
|
if ($existing) {
|
|
throw ValidationException::withMessages([
|
|
'attendance' => 'Anda sudah melakukan presensi masuk hari ini.',
|
|
]);
|
|
}
|
|
|
|
$attendance = Attendance::create([
|
|
'employee_id' => $employee->id,
|
|
'attendance_date' => today(),
|
|
'check_in_at' => now(),
|
|
'check_in_latitude' => $validated['latitude'],
|
|
'check_in_longitude' => $validated['longitude'],
|
|
]);
|
|
|
|
$this->mediaService->addBase64Image(
|
|
$attendance,
|
|
$validated['photo'],
|
|
'checkin',
|
|
'checkin',
|
|
);
|
|
},
|
|
'Gagal mencatat presensi masuk',
|
|
);
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
'⏰ Presensi Masuk',
|
|
"Karyawan {$user->profile?->full_name} melakukan presensi masuk.",
|
|
['owner', 'developer', 'direktur'],
|
|
route('admin.hr.attendances.index'),
|
|
);
|
|
}
|
|
|
|
public function checkOut(array $validated, User $user): void
|
|
{
|
|
$employee = $this->resolveAuthenticatedEmployee($user);
|
|
|
|
$workDurationMinutes = $this->runInTransaction(
|
|
function () use ($employee, $validated): int {
|
|
$attendance = Attendance::query()
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('attendance_date', today())
|
|
->first();
|
|
|
|
if ($attendance === null) {
|
|
throw ValidationException::withMessages([
|
|
'attendance' => 'Anda belum melakukan presensi masuk hari ini.',
|
|
]);
|
|
}
|
|
|
|
if ($attendance->check_out_at !== null) {
|
|
throw ValidationException::withMessages([
|
|
'attendance' => 'Anda sudah melakukan presensi pulang hari ini.',
|
|
]);
|
|
}
|
|
|
|
$checkOutAt = now();
|
|
$workDurationMinutes = (int) $attendance->check_in_at->diffInMinutes($checkOutAt);
|
|
|
|
$attendance->update([
|
|
'check_out_at' => $checkOutAt,
|
|
'check_out_latitude' => $validated['latitude'],
|
|
'check_out_longitude' => $validated['longitude'],
|
|
'work_duration_minutes' => $workDurationMinutes,
|
|
]);
|
|
|
|
$this->mediaService->addBase64Image(
|
|
$attendance,
|
|
$validated['photo'],
|
|
'checkout',
|
|
'checkout',
|
|
);
|
|
|
|
return $workDurationMinutes;
|
|
},
|
|
'Gagal mencatat presensi pulang',
|
|
);
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
'⏰ Presensi Pulang',
|
|
"Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).',
|
|
['owner', 'developer', 'direktur'],
|
|
route('admin.hr.attendances.index'),
|
|
);
|
|
}
|
|
|
|
public function delete(Attendance $attendance, User $user): void
|
|
{
|
|
$attendance->loadMissing('employee.user.profile');
|
|
$employeeName = $attendance->employee?->user?->profile?->full_name;
|
|
$date = $attendance->attendance_date;
|
|
|
|
$this->runInTransaction(
|
|
function () use ($attendance): void {
|
|
$attendance->delete();
|
|
},
|
|
'Gagal menghapus presensi',
|
|
);
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
'🗑️ Presensi Dihapus',
|
|
"Data presensi {$employeeName} tanggal {$date?->toDateString()} telah dihapus oleh {$user->profile?->full_name}.",
|
|
['owner', 'developer', 'direktur'],
|
|
route('admin.hr.attendances.index'),
|
|
);
|
|
}
|
|
|
|
public function resolveAttendanceScope(User $user): array
|
|
{
|
|
$employee = $user->employee;
|
|
$isManager = $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]);
|
|
|
|
return [
|
|
'scopedEmployeeId' => $isManager ? null : $employee?->id,
|
|
'hasScopedAccess' => $isManager || $employee !== null,
|
|
'isManager' => $isManager,
|
|
'employee' => $employee,
|
|
'canCheckIn' => $user->can(Permission::ATTENDANCES_CREATE->value) && $employee !== null,
|
|
];
|
|
}
|
|
|
|
public function indexPageData(User $user, CarbonInterface $start, CarbonInterface $end): array
|
|
{
|
|
$scope = $this->resolveAttendanceScope($user);
|
|
$employee = $scope['employee'];
|
|
|
|
return [
|
|
'attendances' => $this->listForCalendar(
|
|
$start,
|
|
$end,
|
|
$scope['scopedEmployeeId'],
|
|
$scope['hasScopedAccess'],
|
|
),
|
|
'todayAttendance' => $employee
|
|
? $this->todayAttendanceForEmployee($employee)
|
|
: null,
|
|
'isOnLeave' => $employee
|
|
? $this->isOnLeaveToday($employee)
|
|
: false,
|
|
'canCheckIn' => $scope['canCheckIn'],
|
|
'canManageAll' => $scope['isManager'],
|
|
'calendarRange' => [
|
|
'start' => $start->toDateString(),
|
|
'end' => $end->toDateString(),
|
|
],
|
|
];
|
|
}
|
|
}
|