205 lines
6.9 KiB
PHP
205 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Enums\PayrollAdjustmentType;
|
|
use App\Enums\Role;
|
|
use App\Models\Attendance;
|
|
use App\Models\Employee;
|
|
use App\Models\LeaveRequest;
|
|
use App\Models\Payroll;
|
|
use App\Models\PayrollPeriod;
|
|
use App\Models\User;
|
|
use App\Settings\HrSettings;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ApplyAttendancePenalties extends Command
|
|
{
|
|
protected $signature = 'attendance:apply-penalties {--date= : Tanggal yang diproses (default: kemarin)}';
|
|
|
|
protected $description = 'Buat potongan gaji otomatis untuk keterlambatan dan bolos';
|
|
|
|
public function handle(): int
|
|
{
|
|
$date = $this->option('date')
|
|
? Carbon::parse($this->option('date'))->startOfDay()
|
|
: Carbon::yesterday()->startOfDay();
|
|
|
|
$settings = app(HrSettings::class);
|
|
$latePenalty = $settings->late_penalty_amount;
|
|
$absentPenalty = $settings->absent_penalty_amount;
|
|
$scheduledTime = $settings->scheduled_check_in_time;
|
|
|
|
if ($latePenalty <= 0 && $absentPenalty <= 0) {
|
|
$this->info('Denda keterlambatan dan bolos belum diatur (0). Lewatkan.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$scheduledCheckIn = Carbon::createFromFormat('Y-m-d H:i', $date->format('Y-m-d').' '.$scheduledTime);
|
|
$createdBy = $this->resolveSystemUser();
|
|
|
|
if ($createdBy === null) {
|
|
$this->error('Tidak ditemukan user sistem (developer/owner) untuk mencatat penalti.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$employees = Employee::query()
|
|
->whereHas('user', fn ($query) => $query->active())
|
|
->get();
|
|
|
|
$lateCount = 0;
|
|
$absentCount = 0;
|
|
$skippedLeave = 0;
|
|
$skippedDuplicate = 0;
|
|
|
|
try {
|
|
DB::transaction(function () use ($employees, $date, $scheduledCheckIn, $latePenalty, $absentPenalty, $createdBy, &$lateCount, &$absentCount, &$skippedLeave, &$skippedDuplicate): void {
|
|
foreach ($employees as $employee) {
|
|
$hasLeave = LeaveRequest::query()
|
|
->approved()
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('start_date', '<=', $date)
|
|
->whereDate('end_date', '>=', $date)
|
|
->exists();
|
|
|
|
if ($hasLeave) {
|
|
$skippedLeave++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$attendance = Attendance::query()
|
|
->where('employee_id', $employee->id)
|
|
->whereDate('attendance_date', $date)
|
|
->first();
|
|
|
|
if ($attendance === null) {
|
|
if ($absentPenalty <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$payroll = $this->findOpenPayroll($employee->id);
|
|
|
|
if ($payroll === null) {
|
|
continue;
|
|
}
|
|
|
|
$alreadyExists = $payroll->adjustments()
|
|
->whereNull('attendance_id')
|
|
->deduction()
|
|
->where('description', 'like', "%Bolos {$date->format('d/m/Y')}%")
|
|
->exists();
|
|
|
|
if ($alreadyExists) {
|
|
$skippedDuplicate++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$payroll->adjustments()->create([
|
|
'type' => PayrollAdjustmentType::DEDUCTION,
|
|
'amount' => $absentPenalty,
|
|
'description' => "Bolos {$date->format('d/m/Y')}",
|
|
'created_by_id' => $createdBy->id,
|
|
]);
|
|
|
|
$payroll->load('adjustments');
|
|
$payroll->recalculateAmounts();
|
|
$payroll->save();
|
|
|
|
$absentCount++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($latePenalty <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$checkInTime = Carbon::parse($attendance->check_in_at);
|
|
|
|
if ($checkInTime->lte($scheduledCheckIn)) {
|
|
continue;
|
|
}
|
|
|
|
$payroll = $this->findOpenPayroll($employee->id);
|
|
|
|
if ($payroll === null) {
|
|
continue;
|
|
}
|
|
|
|
$alreadyExists = $payroll->adjustments()
|
|
->where('attendance_id', $attendance->id)
|
|
->where('type', PayrollAdjustmentType::DEDUCTION)
|
|
->exists();
|
|
|
|
if ($alreadyExists) {
|
|
$skippedDuplicate++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$minutesLate = (int) $scheduledCheckIn->diffInMinutes($checkInTime);
|
|
|
|
$payroll->adjustments()->create([
|
|
'type' => PayrollAdjustmentType::DEDUCTION,
|
|
'amount' => $latePenalty,
|
|
'description' => "Terlambat {$date->format('d/m/Y')} ({$minutesLate} menit)",
|
|
'attendance_id' => $attendance->id,
|
|
'created_by_id' => $createdBy->id,
|
|
]);
|
|
|
|
$payroll->load('adjustments');
|
|
$payroll->recalculateAmounts();
|
|
$payroll->save();
|
|
|
|
$lateCount++;
|
|
}
|
|
});
|
|
} catch (\Throwable $e) {
|
|
Log::error("Gagal menerapkan penalti presensi: {$e->getMessage()}", [
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
|
|
$this->error('Gagal menerapkan penalti presensi: '.$e->getMessage());
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->info("Selesai memproses tanggal {$date->format('d/m/Y')}:");
|
|
$this->info(" - Terlambat: {$lateCount} pegawai");
|
|
$this->info(" - Bolos: {$absentCount} pegawai");
|
|
$this->info(" - Lewati (cuti): {$skippedLeave} pegawai");
|
|
$this->info(" - Lewati (duplikat): {$skippedDuplicate} pegawai");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function findOpenPayroll(int $employeeId): ?Payroll
|
|
{
|
|
$period = PayrollPeriod::query()->open()->first();
|
|
|
|
if ($period === null) {
|
|
return null;
|
|
}
|
|
|
|
return Payroll::query()
|
|
->where('payroll_period_id', $period->id)
|
|
->where('employee_id', $employeeId)
|
|
->first();
|
|
}
|
|
|
|
private function resolveSystemUser(): ?User
|
|
{
|
|
return User::query()
|
|
->whereHas('roles', fn ($query) => $query->whereIn('name', [Role::DEVELOPER->value, Role::OWNER->value]))
|
|
->first()
|
|
?? User::query()->first();
|
|
}
|
|
}
|