- 월간 캘린더 뷰 (사원별 필터, 날짜 클릭 등록, HTMX 월 이동) - 일괄 등록 (다수 사원 체크박스 선택 후 일괄 등록, upsert 처리) - 사원별 월간 요약 (상태별 카운트 + 총 근무시간 집계 테이블) - 초과근무 알림 (주 48h 경고 / 52h 위험 배너) - 근태 승인 워크플로우 (신청→승인→근태 레코드 자동 생성) - 자동 결근 처리 (매일 23:50 스케줄러, 주말 제외) - 연차 관리 연동 (휴가 등록 시 leave_balances 자동 차감) - GPS 출퇴근 UI (테이블 GPS 아이콘 + 상세 모달) - 탭 네비게이션 (목록/캘린더/요약/승인) HTMX 기반 전환
149 lines
4.2 KiB
PHP
149 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\HR;
|
|
|
|
use App\Models\HR\Attendance;
|
|
use App\Models\HR\AttendanceRequest;
|
|
use Carbon\CarbonPeriod;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AttendanceRequestService
|
|
{
|
|
/**
|
|
* 신청 목록 조회
|
|
*/
|
|
public function getRequests(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = AttendanceRequest::query()
|
|
->with(['user', 'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId), 'approver'])
|
|
->forTenant($tenantId)
|
|
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected')")
|
|
->orderBy('created_at', 'desc');
|
|
|
|
if (! empty($filters['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
if (! empty($filters['user_id'])) {
|
|
$query->where('user_id', $filters['user_id']);
|
|
}
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 신청 등록
|
|
*/
|
|
public function storeRequest(array $data): AttendanceRequest
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return AttendanceRequest::create([
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $data['user_id'],
|
|
'request_type' => $data['request_type'],
|
|
'start_date' => $data['start_date'],
|
|
'end_date' => $data['end_date'],
|
|
'reason' => $data['reason'] ?? null,
|
|
'status' => 'pending',
|
|
'json_details' => $data['json_details'] ?? null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 승인 처리
|
|
*/
|
|
public function approve(int $id): ?AttendanceRequest
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$request = AttendanceRequest::query()
|
|
->forTenant($tenantId)
|
|
->where('status', 'pending')
|
|
->find($id);
|
|
|
|
if (! $request) {
|
|
return null;
|
|
}
|
|
|
|
return DB::transaction(function () use ($request, $tenantId) {
|
|
$request->update([
|
|
'status' => 'approved',
|
|
'approved_by' => auth()->id(),
|
|
'approved_at' => now(),
|
|
]);
|
|
|
|
// 승인 시 해당 기간의 근태 레코드 자동 생성
|
|
$this->createAttendanceRecords($request, $tenantId);
|
|
|
|
return $request->fresh(['user', 'approver']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 반려 처리
|
|
*/
|
|
public function reject(int $id, ?string $reason = null): ?AttendanceRequest
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$request = AttendanceRequest::query()
|
|
->forTenant($tenantId)
|
|
->where('status', 'pending')
|
|
->find($id);
|
|
|
|
if (! $request) {
|
|
return null;
|
|
}
|
|
|
|
$request->update([
|
|
'status' => 'rejected',
|
|
'approved_by' => auth()->id(),
|
|
'approved_at' => now(),
|
|
'reject_reason' => $reason,
|
|
]);
|
|
|
|
return $request->fresh(['user', 'approver']);
|
|
}
|
|
|
|
/**
|
|
* 승인 후 근태 레코드 자동 생성
|
|
*/
|
|
private function createAttendanceRecords(AttendanceRequest $request, int $tenantId): void
|
|
{
|
|
$statusMap = [
|
|
'vacation' => 'vacation',
|
|
'businessTrip' => 'businessTrip',
|
|
'remote' => 'remote',
|
|
'fieldWork' => 'fieldWork',
|
|
];
|
|
|
|
$status = $statusMap[$request->request_type] ?? $request->request_type;
|
|
|
|
$period = CarbonPeriod::create($request->start_date, $request->end_date);
|
|
|
|
foreach ($period as $date) {
|
|
// 주말 제외
|
|
if ($date->isWeekend()) {
|
|
continue;
|
|
}
|
|
|
|
Attendance::updateOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $request->user_id,
|
|
'base_date' => $date->toDateString(),
|
|
],
|
|
[
|
|
'status' => $status,
|
|
'remarks' => $request->reason ? mb_substr($request->reason, 0, 100) : null,
|
|
'updated_by' => auth()->id(),
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|