feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Services\HR;
|
|
|
|
|
|
|
2026-02-28 15:54:41 +09:00
|
|
|
|
use App\Models\Approvals\Approval;
|
|
|
|
|
|
use App\Models\Approvals\ApprovalForm;
|
|
|
|
|
|
use App\Models\Approvals\ApprovalLine;
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
use App\Models\HR\Attendance;
|
2026-02-27 11:09:07 +09:00
|
|
|
|
use App\Models\HR\Employee;
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
use App\Models\HR\Leave;
|
|
|
|
|
|
use App\Models\HR\LeaveBalance;
|
2026-02-27 11:09:07 +09:00
|
|
|
|
use App\Models\HR\LeavePolicy;
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
use App\Models\Tenants\Department;
|
2026-02-28 15:54:41 +09:00
|
|
|
|
use App\Services\ApprovalService;
|
2026-02-27 11:09:07 +09:00
|
|
|
|
use Carbon\Carbon;
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
use Carbon\CarbonPeriod;
|
|
|
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
|
|
class LeaveService
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 휴가 목록 조회 (필터 + 페이지네이션)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getLeaves(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$query = Leave::query()
|
|
|
|
|
|
->with([
|
|
|
|
|
|
'user',
|
|
|
|
|
|
'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
|
|
|
|
'user.tenantProfiles.department',
|
|
|
|
|
|
'approver',
|
2026-03-03 22:36:05 +09:00
|
|
|
|
'approval.steps.approver',
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
])
|
|
|
|
|
|
->forTenant($tenantId);
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['q'])) {
|
|
|
|
|
|
$search = $filters['q'];
|
|
|
|
|
|
$query->whereHas('user', fn ($q) => $q->where('name', 'like', "%{$search}%"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['user_id'])) {
|
|
|
|
|
|
$query->where('user_id', $filters['user_id']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['department_id'])) {
|
|
|
|
|
|
$deptId = $filters['department_id'];
|
|
|
|
|
|
$query->whereHas('user.tenantProfiles', function ($q) use ($tenantId, $deptId) {
|
|
|
|
|
|
$q->where('tenant_id', $tenantId)->where('department_id', $deptId);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['leave_type'])) {
|
|
|
|
|
|
$query->where('leave_type', $filters['leave_type']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['status'])) {
|
|
|
|
|
|
$query->where('status', $filters['status']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['date_from']) && ! empty($filters['date_to'])) {
|
|
|
|
|
|
$query->betweenDates($filters['date_from'], $filters['date_to']);
|
|
|
|
|
|
} elseif (! empty($filters['date_from'])) {
|
|
|
|
|
|
$query->where('start_date', '>=', $filters['date_from']);
|
|
|
|
|
|
} elseif (! empty($filters['date_to'])) {
|
|
|
|
|
|
$query->where('end_date', '<=', $filters['date_to']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $query
|
|
|
|
|
|
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected', 'cancelled')")
|
|
|
|
|
|
->orderBy('created_at', 'desc')
|
|
|
|
|
|
->paginate($perPage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 휴가/근태신청/사유서 등록 → 결재 자동 생성 + 상신
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function storeLeave(array $data): Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
2026-03-03 23:50:27 +09:00
|
|
|
|
$leaveType = $data['leave_type'];
|
|
|
|
|
|
|
|
|
|
|
|
// 사유서는 days=0, 그 외는 자동 계산
|
|
|
|
|
|
if (in_array($leaveType, Leave::REASON_REPORT_TYPES)) {
|
|
|
|
|
|
$days = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$days = $this->calculateDays($leaveType, $data['start_date'], $data['end_date']);
|
|
|
|
|
|
}
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
|
|
|
|
|
|
// 연차 차감 대상이면 잔여일수 검증
|
2026-03-03 23:50:27 +09:00
|
|
|
|
if (in_array($leaveType, Leave::DEDUCTIBLE_TYPES)) {
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($data['user_id'])
|
|
|
|
|
|
->forYear(now()->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (! $balance || ! $balance->canUse($days)) {
|
|
|
|
|
|
throw new \RuntimeException('잔여 연차가 부족합니다.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
return DB::transaction(function () use ($data, $tenantId, $days, $leaveType) {
|
2026-02-28 15:54:41 +09:00
|
|
|
|
$leave = Leave::create([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'user_id' => $data['user_id'],
|
2026-03-03 23:50:27 +09:00
|
|
|
|
'leave_type' => $leaveType,
|
2026-02-28 15:54:41 +09:00
|
|
|
|
'start_date' => $data['start_date'],
|
|
|
|
|
|
'end_date' => $data['end_date'],
|
|
|
|
|
|
'days' => $days,
|
|
|
|
|
|
'reason' => $data['reason'] ?? null,
|
|
|
|
|
|
'status' => 'pending',
|
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 결재 자동 생성 + 상신 (유형에 맞는 결재양식 자동 선택)
|
2026-03-03 22:36:05 +09:00
|
|
|
|
$approval = $this->createLeaveApproval($leave, $tenantId, $data['approval_line_id'] ?? null);
|
2026-02-28 15:54:41 +09:00
|
|
|
|
$leave->update(['approval_id' => $approval->id]);
|
|
|
|
|
|
|
|
|
|
|
|
return $leave;
|
|
|
|
|
|
});
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 승인 → LeaveBalance 차감 → 유형별 Attendance 반영
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function approve(int $id): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->withStatus('pending')
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($leave, $tenantId) {
|
|
|
|
|
|
$leave->update([
|
|
|
|
|
|
'status' => 'approved',
|
|
|
|
|
|
'approved_by' => auth()->id(),
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 연차 차감 대상이면 LeaveBalance 차감
|
|
|
|
|
|
if ($leave->is_deductible) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($leave->user_id)
|
|
|
|
|
|
->forYear($leave->start_date->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($balance) {
|
|
|
|
|
|
$balance->useLeave($leave->days);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 유형별 Attendance 반영
|
|
|
|
|
|
$this->applyAttendanceByType($leave, $tenantId);
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
|
|
|
|
|
|
return $leave->fresh(['user', 'approver']);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 반려
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function reject(int $id, ?string $reason = null): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->withStatus('pending')
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$leave->update([
|
|
|
|
|
|
'status' => 'rejected',
|
|
|
|
|
|
'approved_by' => auth()->id(),
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
'reject_reason' => $reason,
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return $leave->fresh(['user', 'approver']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-28 15:54:41 +09:00
|
|
|
|
* 취소 → LeaveBalance 복원 → Attendance 삭제 → 결재 취소
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function cancel(int $id): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->withStatus('approved')
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($leave, $tenantId) {
|
|
|
|
|
|
$leave->update([
|
|
|
|
|
|
'status' => 'cancelled',
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 연차 차감 대상이면 LeaveBalance 복원
|
|
|
|
|
|
if ($leave->is_deductible) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($leave->user_id)
|
|
|
|
|
|
->forYear($leave->start_date->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($balance) {
|
|
|
|
|
|
$balance->restoreLeave($leave->days);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 해당 기간 vacation Attendance soft delete
|
|
|
|
|
|
$this->deleteAttendanceRecords($leave, $tenantId);
|
|
|
|
|
|
|
|
|
|
|
|
return $leave->fresh(['user']);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 15:54:41 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* pending 상태 휴가 삭제 → 연결된 결재 취소
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function deletePendingLeave(int $id): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->withStatus('pending')
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($leave) {
|
|
|
|
|
|
// 연결된 결재가 있고 아직 진행 중이면 취소
|
|
|
|
|
|
if ($leave->approval_id) {
|
|
|
|
|
|
$approval = Approval::find($leave->approval_id);
|
|
|
|
|
|
if ($approval && in_array($approval->status, ['draft', 'pending'])) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
app(ApprovalService::class)->cancel($approval->id);
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
// 결재 취소 실패해도 휴가 삭제는 진행
|
|
|
|
|
|
report($e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$leave->update(['deleted_by' => auth()->id()]);
|
|
|
|
|
|
$leave->delete();
|
|
|
|
|
|
|
|
|
|
|
|
return $leave;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-04 00:15:41 +09:00
|
|
|
|
* 휴가/신청 삭제 (슈퍼관리자 전용 — 모든 상태 허용)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function deleteLeave(int $id): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($leave, $tenantId) {
|
|
|
|
|
|
// 연결된 결재 취소 시도
|
|
|
|
|
|
if ($leave->approval_id) {
|
|
|
|
|
|
$approval = Approval::find($leave->approval_id);
|
|
|
|
|
|
if ($approval && in_array($approval->status, ['draft', 'pending'])) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
app(ApprovalService::class)->cancel($approval->id);
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
report($e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 승인된 연차 차감 복원
|
|
|
|
|
|
if ($leave->status === 'approved' && $leave->is_deductible) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($leave->user_id)
|
|
|
|
|
|
->forYear($leave->start_date->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
$balance?->restoreLeave($leave->days);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$leave->update(['deleted_by' => auth()->id()]);
|
|
|
|
|
|
$leave->delete();
|
|
|
|
|
|
|
|
|
|
|
|
return $leave;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 휴가/신청 영구삭제 (슈퍼관리자 전용)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function forceDeleteLeave(int $id): ?Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$leave = Leave::withTrashed()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->find($id);
|
|
|
|
|
|
|
|
|
|
|
|
if (! $leave) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($leave, $tenantId) {
|
|
|
|
|
|
// 승인된 연차 차감 복원 (아직 soft-deleted 아닌 경우)
|
|
|
|
|
|
if ($leave->status === 'approved' && $leave->is_deductible && ! $leave->trashed()) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($leave->user_id)
|
|
|
|
|
|
->forYear($leave->start_date->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
$balance?->restoreLeave($leave->days);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 연결된 결재 정리
|
|
|
|
|
|
if ($leave->approval_id) {
|
|
|
|
|
|
$approval = Approval::withTrashed()->find($leave->approval_id);
|
|
|
|
|
|
$approval?->forceDelete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$leave->forceDelete();
|
|
|
|
|
|
|
|
|
|
|
|
return $leave;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 결재 승인에 의한 휴가/근태신청/사유서 자동 승인
|
2026-02-28 15:54:41 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function approveByApproval(Leave $leave, Approval $approval): Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = $leave->tenant_id;
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 최종 결재자 ID 찾기
|
2026-02-28 16:01:36 +09:00
|
|
|
|
$lastApprover = $approval->steps()
|
2026-02-28 15:54:41 +09:00
|
|
|
|
->where('status', 'approved')
|
2026-02-28 16:03:28 +09:00
|
|
|
|
->reorder('step_order', 'desc')
|
2026-02-28 15:54:41 +09:00
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
$leave->update([
|
|
|
|
|
|
'status' => 'approved',
|
|
|
|
|
|
'approved_by' => $lastApprover?->approver_id ?? auth()->id(),
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 연차 차감 (DEDUCTIBLE_TYPES만)
|
2026-02-28 15:54:41 +09:00
|
|
|
|
if ($leave->is_deductible) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('year', $leave->start_date->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($balance) {
|
|
|
|
|
|
$balance->useLeave($leave->days);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 유형별 Attendance 반영
|
|
|
|
|
|
$this->applyAttendanceByType($leave, $tenantId);
|
2026-02-28 15:54:41 +09:00
|
|
|
|
|
|
|
|
|
|
return $leave->fresh(['user', 'approver']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 유형별 Attendance 반영 분기
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function applyAttendanceByType(Leave $leave, int $tenantId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$attendanceStatus = Leave::ATTENDANCE_STATUS_MAP[$leave->leave_type] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if ($attendanceStatus) {
|
|
|
|
|
|
// 휴가/출장/재택/외근 → Attendance 자동 생성
|
|
|
|
|
|
$this->createAttendanceRecords($leave, $tenantId, $attendanceStatus);
|
|
|
|
|
|
} elseif ($leave->leave_type === 'early_leave') {
|
|
|
|
|
|
// 조퇴 → 기존 Attendance에 비고 기록
|
|
|
|
|
|
$this->markEarlyLeave($leave, $tenantId);
|
|
|
|
|
|
} elseif (in_array($leave->leave_type, Leave::REASON_REPORT_TYPES)) {
|
|
|
|
|
|
// 지각/결근 사유서 → 기존 Attendance에 사유 기록
|
|
|
|
|
|
$this->addReasonToAttendance($leave, $tenantId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 조퇴 승인 시 기존 Attendance에 비고 기록
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function markEarlyLeave(Leave $leave, int $tenantId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$attendance = Attendance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('base_date', $leave->start_date->toDateString())
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($attendance) {
|
|
|
|
|
|
$remarks = $attendance->remarks;
|
|
|
|
|
|
$reason = $leave->reason ? " ({$leave->reason})" : '';
|
|
|
|
|
|
$attendance->update([
|
|
|
|
|
|
'remarks' => trim(($remarks ? $remarks.' / ' : '').'조퇴'.$reason),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 지각/결근 사유서 승인 시 기존 Attendance에 사유 기록
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function addReasonToAttendance(Leave $leave, int $tenantId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$attendance = Attendance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('base_date', $leave->start_date->toDateString())
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($attendance) {
|
|
|
|
|
|
$typeName = $leave->leave_type === 'late_reason' ? '지각사유' : '결근사유';
|
|
|
|
|
|
$reason = $leave->reason ?? '';
|
|
|
|
|
|
$remarks = $attendance->remarks;
|
|
|
|
|
|
$attendance->update([
|
|
|
|
|
|
'remarks' => trim(($remarks ? $remarks.' / ' : '')."[{$typeName}] {$reason}"),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 15:54:41 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 결재 반려에 의한 휴가 자동 반려
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function rejectByApproval(Leave $leave, string $comment, int $rejecterId): Leave
|
|
|
|
|
|
{
|
|
|
|
|
|
$leave->update([
|
|
|
|
|
|
'status' => 'rejected',
|
|
|
|
|
|
'reject_reason' => $comment,
|
|
|
|
|
|
'approved_by' => $rejecterId,
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return $leave->fresh(['user', 'approver']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 전체 사원 잔여연차 요약
|
2026-02-27 11:09:07 +09:00
|
|
|
|
*
|
|
|
|
|
|
* 사원관리의 모든 재직/휴직 직원을 표시하며,
|
|
|
|
|
|
* balance 레코드가 없는 직원은 자동 생성한다.
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
2026-03-05 15:16:54 +09:00
|
|
|
|
public function getBalanceSummary(?int $year = null, ?string $sort = null, ?string $direction = null, ?string $empStatus = null): Collection
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
$year = $year ?? now()->year;
|
|
|
|
|
|
|
2026-02-27 11:09:07 +09:00
|
|
|
|
// (1) 테넌트 연차 정책 조회
|
|
|
|
|
|
$policy = LeavePolicy::forTenant($tenantId)->first();
|
|
|
|
|
|
|
2026-03-05 15:16:54 +09:00
|
|
|
|
// (2) 재직상태 필터에 따른 직원 조회
|
|
|
|
|
|
$statusFilter = match ($empStatus) {
|
|
|
|
|
|
'active' => ['active', 'leave'],
|
|
|
|
|
|
'resigned' => ['resigned'],
|
|
|
|
|
|
default => ['active', 'leave', 'resigned'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-27 11:09:07 +09:00
|
|
|
|
$employees = Employee::query()
|
|
|
|
|
|
->with(['user:id,name', 'department:id,name'])
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
->forTenant($tenantId)
|
2026-03-05 15:16:54 +09:00
|
|
|
|
->whereIn('employee_status', $statusFilter)
|
2026-03-05 17:08:41 +09:00
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereDoesntHave('department', function ($dq) {
|
|
|
|
|
|
$dq->where('name', 'like', '%영업팀%');
|
|
|
|
|
|
})->orWhereNull('department_id');
|
|
|
|
|
|
})
|
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereNull('json_extra->is_excluded')
|
|
|
|
|
|
->orWhere('json_extra->is_excluded', false);
|
|
|
|
|
|
})
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
->get();
|
2026-02-27 11:09:07 +09:00
|
|
|
|
|
|
|
|
|
|
// (3) 기존 balance 일괄 조회
|
|
|
|
|
|
$existingBalances = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forYear($year)
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('user_id');
|
|
|
|
|
|
|
|
|
|
|
|
// (4) balance 없는 직원 → insertOrIgnore로 자동 생성
|
|
|
|
|
|
$newRecords = [];
|
|
|
|
|
|
foreach ($employees as $employee) {
|
|
|
|
|
|
if (! $existingBalances->has($employee->user_id)) {
|
|
|
|
|
|
$newRecords[] = [
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'user_id' => $employee->user_id,
|
|
|
|
|
|
'year' => $year,
|
|
|
|
|
|
'total_days' => $this->calculateAnnualLeaveDays($employee, $year, $policy),
|
|
|
|
|
|
'used_days' => 0,
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($newRecords)) {
|
|
|
|
|
|
LeaveBalance::insertOrIgnore($newRecords);
|
|
|
|
|
|
|
|
|
|
|
|
// 새로 생성된 레코드 포함하여 다시 조회
|
|
|
|
|
|
$existingBalances = LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forYear($year)
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('user_id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// (5) Employee 정보를 balance에 연결
|
2026-02-27 11:09:07 +09:00
|
|
|
|
$employeesByUserId = $employees->keyBy('user_id');
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// (6) 현재연도 + 1년 미만 직원은 매번 재계산 (월별 발생 방식)
|
2026-02-27 13:16:08 +09:00
|
|
|
|
// 퇴사자는 퇴사일 기준으로 확정되므로 재계산 대상에서 제외
|
2026-02-27 12:42:10 +09:00
|
|
|
|
if ($year === (int) now()->year) {
|
|
|
|
|
|
foreach ($existingBalances as $balance) {
|
|
|
|
|
|
$employee = $employeesByUserId->get($balance->user_id);
|
|
|
|
|
|
if (! $employee || ! $employee->hire_date) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:16:08 +09:00
|
|
|
|
if ($employee->employee_status === 'resigned') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
$hire = Carbon::parse($employee->hire_date);
|
|
|
|
|
|
if ($hire->diffInMonths(today()) < 12) {
|
|
|
|
|
|
$recalculated = $this->calculateAnnualLeaveDays($employee, $year, $policy);
|
|
|
|
|
|
if (abs($balance->total_days - $recalculated) > 0.001) {
|
|
|
|
|
|
$balance->update(['total_days' => $recalculated]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:06:42 +09:00
|
|
|
|
$result = $existingBalances
|
2026-02-27 11:09:07 +09:00
|
|
|
|
->filter(fn ($balance) => $employeesByUserId->has($balance->user_id))
|
|
|
|
|
|
->map(function ($balance) use ($employeesByUserId) {
|
|
|
|
|
|
$employee = $employeesByUserId->get($balance->user_id);
|
|
|
|
|
|
$balance->employee = $employee;
|
|
|
|
|
|
|
|
|
|
|
|
return $balance;
|
2026-02-27 13:06:42 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 정렬 (기본: 입사일 오름차순)
|
|
|
|
|
|
$sortField = $sort ?? 'hire_date';
|
|
|
|
|
|
$isDesc = ($direction ?? 'asc') === 'desc';
|
|
|
|
|
|
|
|
|
|
|
|
$sortCallback = match ($sortField) {
|
|
|
|
|
|
'name' => fn ($b) => $b->employee?->display_name ?? '',
|
|
|
|
|
|
'department' => fn ($b) => $b->employee?->department?->name ?? '',
|
|
|
|
|
|
'hire_date' => fn ($b) => $b->employee?->hire_date ?? '9999-12-31',
|
2026-02-27 13:16:08 +09:00
|
|
|
|
'status' => fn ($b) => match ($b->employee?->employee_status) {
|
|
|
|
|
|
'active' => 1, 'leave' => 2, 'resigned' => 3, default => 9,
|
|
|
|
|
|
},
|
|
|
|
|
|
'resign_date' => fn ($b) => $b->employee?->resign_date ?? '9999-12-31',
|
2026-02-27 13:06:42 +09:00
|
|
|
|
'total_days' => fn ($b) => $b->total_days,
|
|
|
|
|
|
'used_days' => fn ($b) => $b->used_days,
|
|
|
|
|
|
'remaining' => fn ($b) => $b->total_days - $b->used_days,
|
|
|
|
|
|
'rate' => fn ($b) => $b->total_days > 0 ? $b->used_days / $b->total_days : 0,
|
|
|
|
|
|
default => fn ($b) => $b->employee?->hire_date ?? '9999-12-31',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return ($isDesc ? $result->sortByDesc($sortCallback) : $result->sortBy($sortCallback))->values();
|
2026-02-27 11:09:07 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 입사일 기반 연차일수 자동 산출 (근로기준법 제60조)
|
|
|
|
|
|
*
|
|
|
|
|
|
* - 입사일 없음 → default_annual_leave (기본 15일)
|
2026-02-27 12:42:10 +09:00
|
|
|
|
* - 1년 미만 → 입사일~기준일 완료 월수 (최대 11일, 월별 발생)
|
2026-02-27 11:09:07 +09:00
|
|
|
|
* - 1년 이상 → 15일 + 매 2년마다 +1일 (최대 max_annual_leave)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function calculateAnnualLeaveDays(Employee $employee, int $year, ?LeavePolicy $policy): float
|
|
|
|
|
|
{
|
|
|
|
|
|
$defaultDays = $policy->default_annual_leave ?? 15;
|
|
|
|
|
|
$maxDays = $policy->max_annual_leave ?? 25;
|
|
|
|
|
|
|
|
|
|
|
|
$hireDate = $employee->hire_date;
|
|
|
|
|
|
if (! $hireDate) {
|
|
|
|
|
|
return (float) $defaultDays;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$hire = Carbon::parse($hireDate);
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// 기준일: 현재연도면 오늘, 과거연도면 연말
|
|
|
|
|
|
$referenceDate = $year === (int) now()->year
|
|
|
|
|
|
? today()
|
|
|
|
|
|
: Carbon::create($year, 12, 31);
|
2026-02-27 11:09:07 +09:00
|
|
|
|
|
2026-02-27 13:16:08 +09:00
|
|
|
|
// 퇴사자: 기준일을 퇴사일로 제한
|
|
|
|
|
|
$resignDate = $employee->resign_date;
|
|
|
|
|
|
if ($resignDate) {
|
|
|
|
|
|
$resign = Carbon::parse($resignDate);
|
|
|
|
|
|
if ($resign->lessThan($referenceDate)) {
|
|
|
|
|
|
$referenceDate = $resign;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// 아직 입사 전이면 0일
|
|
|
|
|
|
if ($hire->greaterThan($referenceDate)) {
|
|
|
|
|
|
return 0;
|
2026-02-27 11:09:07 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// 입사일~기준일 완료 월수
|
|
|
|
|
|
$totalMonthsWorked = (int) $hire->diffInMonths($referenceDate);
|
2026-02-27 11:09:07 +09:00
|
|
|
|
|
2026-02-27 12:42:10 +09:00
|
|
|
|
// 1년 미만: 완료된 월수 × 1일 (최대 11일)
|
|
|
|
|
|
if ($totalMonthsWorked < 12) {
|
|
|
|
|
|
return (float) min($totalMonthsWorked, 11);
|
2026-02-27 11:09:07 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1년 이상: 15일 + 매 2년마다 +1일
|
2026-02-27 12:42:10 +09:00
|
|
|
|
$yearsWorked = (int) $hire->diffInYears($referenceDate);
|
2026-02-27 11:09:07 +09:00
|
|
|
|
$additionalDays = (int) floor(($yearsWorked - 1) / 2);
|
|
|
|
|
|
$totalDays = $defaultDays + $additionalDays;
|
|
|
|
|
|
|
|
|
|
|
|
return (float) min($totalDays, $maxDays);
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 개별 사원 잔여연차
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getUserBalance(int $userId, ?int $year = null): ?LeaveBalance
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
$year = $year ?? now()->year;
|
|
|
|
|
|
|
|
|
|
|
|
return LeaveBalance::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forUser($userId)
|
|
|
|
|
|
->forYear($year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 유형별/사원별 사용 통계
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getUsageStats(?int $year = null): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
$year = $year ?? now()->year;
|
|
|
|
|
|
|
|
|
|
|
|
// 유형별 집계
|
|
|
|
|
|
$byType = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forYear($year)
|
|
|
|
|
|
->withStatus('approved')
|
|
|
|
|
|
->select('leave_type', DB::raw('COUNT(*) as count'), DB::raw('SUM(days) as total_days'))
|
|
|
|
|
|
->groupBy('leave_type')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('leave_type');
|
|
|
|
|
|
|
|
|
|
|
|
// 사원별 유형 크로스 테이블
|
|
|
|
|
|
$byUser = Leave::query()
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->forYear($year)
|
|
|
|
|
|
->withStatus('approved')
|
|
|
|
|
|
->with([
|
|
|
|
|
|
'user',
|
|
|
|
|
|
'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
|
|
|
|
'user.tenantProfiles.department',
|
|
|
|
|
|
])
|
|
|
|
|
|
->select('user_id', 'leave_type', DB::raw('SUM(days) as total_days'))
|
|
|
|
|
|
->groupBy('user_id', 'leave_type')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->groupBy('user_id');
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'by_type' => $byType,
|
|
|
|
|
|
'by_user' => $byUser,
|
|
|
|
|
|
'year' => $year,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 일수 자동 계산 (반차 = 0.5, 연차/기타 = 영업일수)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function calculateDays(string $type, string $start, string $end): float
|
|
|
|
|
|
{
|
|
|
|
|
|
if (in_array($type, ['half_am', 'half_pm'])) {
|
|
|
|
|
|
return 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$period = CarbonPeriod::create($start, $end);
|
|
|
|
|
|
$businessDays = 0;
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($period as $date) {
|
|
|
|
|
|
if (! $date->isWeekend()) {
|
|
|
|
|
|
$businessDays++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (float) $businessDays;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* CSV 내보내기용 데이터
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getExportData(array $filters = []): Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
$query = Leave::query()
|
|
|
|
|
|
->with([
|
|
|
|
|
|
'user',
|
|
|
|
|
|
'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
|
|
|
|
'user.tenantProfiles.department',
|
|
|
|
|
|
'approver',
|
|
|
|
|
|
])
|
|
|
|
|
|
->forTenant($tenantId);
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['status'])) {
|
|
|
|
|
|
$query->withStatus($filters['status']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['leave_type'])) {
|
|
|
|
|
|
$query->where('leave_type', $filters['leave_type']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! empty($filters['date_from']) && ! empty($filters['date_to'])) {
|
|
|
|
|
|
$query->betweenDates($filters['date_from'], $filters['date_to']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $query->orderBy('start_date', 'desc')->get();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 부서 목록
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getDepartments(): \Illuminate\Database\Eloquent\Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
return Department::query()
|
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
|
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
2026-03-05 16:54:21 +09:00
|
|
|
|
->where('name', 'not like', '%영업팀%')
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
|
->orderBy('name')
|
|
|
|
|
|
->get(['id', 'name', 'code']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-05 16:54:21 +09:00
|
|
|
|
* 활성 사원 목록 (드롭다운용) — 영업팀 + 강제 제외 사원 제외
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function getActiveEmployees(): \Illuminate\Database\Eloquent\Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
|
|
return \App\Models\HR\Employee::query()
|
|
|
|
|
|
->with('user:id,name')
|
|
|
|
|
|
->forTenant($tenantId)
|
|
|
|
|
|
->activeEmployees()
|
2026-03-05 16:54:21 +09:00
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereDoesntHave('department', function ($dq) {
|
|
|
|
|
|
$dq->where('name', 'like', '%영업팀%');
|
|
|
|
|
|
})->orWhereNull('department_id');
|
|
|
|
|
|
})
|
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereNull('json_extra->is_excluded')
|
|
|
|
|
|
->orWhere('json_extra->is_excluded', false);
|
|
|
|
|
|
})
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
->orderBy('display_name')
|
|
|
|
|
|
->get(['id', 'user_id', 'display_name', 'department_id']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// Private
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
2026-02-28 15:54:41 +09:00
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 휴가/근태신청/사유서 결재 자동 생성 + 상신
|
2026-02-28 15:54:41 +09:00
|
|
|
|
*/
|
2026-03-03 22:36:05 +09:00
|
|
|
|
private function createLeaveApproval(Leave $leave, int $tenantId, ?int $approvalLineId = null): Approval
|
2026-02-28 15:54:41 +09:00
|
|
|
|
{
|
|
|
|
|
|
$approvalService = app(ApprovalService::class);
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 1. 유형에 맞는 결재양식 조회 (FORM_CODE_MAP으로 동적 결정)
|
|
|
|
|
|
$formCode = Leave::FORM_CODE_MAP[$leave->leave_type] ?? 'leave';
|
|
|
|
|
|
$form = ApprovalForm::where('code', $formCode)
|
2026-02-28 15:54:41 +09:00
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (! $form) {
|
2026-03-03 23:50:27 +09:00
|
|
|
|
$formNames = ['leave' => '휴가신청', 'attendance_request' => '근태신청', 'reason_report' => '사유서'];
|
|
|
|
|
|
$formName = $formNames[$formCode] ?? $formCode;
|
|
|
|
|
|
throw new \RuntimeException("{$formName} 결재 양식이 등록되지 않았습니다.");
|
2026-02-28 15:54:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 22:36:05 +09:00
|
|
|
|
// 2. 결재선 조회: 지정된 ID 우선, 없으면 기본결재선
|
|
|
|
|
|
$line = null;
|
|
|
|
|
|
if ($approvalLineId) {
|
|
|
|
|
|
$line = ApprovalLine::where('tenant_id', $tenantId)->find($approvalLineId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (! $line) {
|
|
|
|
|
|
$line = ApprovalLine::where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('is_default', true)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
}
|
2026-02-28 15:54:41 +09:00
|
|
|
|
|
2026-03-03 22:36:05 +09:00
|
|
|
|
if (! $line) {
|
|
|
|
|
|
throw new \RuntimeException('결재선을 찾을 수 없습니다. 기본결재선을 설정하거나 결재선을 선택해주세요.');
|
2026-02-28 15:54:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 결재 본문 생성
|
|
|
|
|
|
$body = $this->buildLeaveApprovalBody($leave, $tenantId);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. steps 변환
|
2026-03-03 22:36:05 +09:00
|
|
|
|
$steps = collect($line->steps)->map(fn ($s) => [
|
2026-02-28 15:54:41 +09:00
|
|
|
|
'user_id' => $s['user_id'],
|
|
|
|
|
|
'step_type' => $s['step_type'] ?? $s['type'] ?? 'approval',
|
|
|
|
|
|
])->toArray();
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 5. 결재 제목 생성 (유형별 차별화)
|
2026-02-28 15:54:41 +09:00
|
|
|
|
$typeName = Leave::TYPE_MAP[$leave->leave_type] ?? $leave->leave_type;
|
|
|
|
|
|
$userName = $leave->user->name ?? '';
|
|
|
|
|
|
$period = $leave->start_date->format('n/j').'~'.$leave->end_date->format('n/j');
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
$titlePrefix = match ($formCode) {
|
|
|
|
|
|
'attendance_request' => '근태신청',
|
|
|
|
|
|
'reason_report' => '사유서',
|
|
|
|
|
|
default => '휴가신청',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 15:54:41 +09:00
|
|
|
|
$approval = $approvalService->createApproval([
|
|
|
|
|
|
'form_id' => $form->id,
|
2026-03-03 22:36:05 +09:00
|
|
|
|
'line_id' => $line->id,
|
2026-03-03 23:50:27 +09:00
|
|
|
|
'title' => "{$titlePrefix} - {$userName} ({$typeName} {$period})",
|
2026-02-28 15:54:41 +09:00
|
|
|
|
'body' => $body,
|
|
|
|
|
|
'content' => [
|
|
|
|
|
|
'leave_id' => $leave->id,
|
|
|
|
|
|
'user_name' => $userName,
|
|
|
|
|
|
'leave_type' => $typeName,
|
|
|
|
|
|
'start_date' => $leave->start_date->toDateString(),
|
|
|
|
|
|
'end_date' => $leave->end_date->toDateString(),
|
|
|
|
|
|
'days' => $leave->days,
|
|
|
|
|
|
'reason' => $leave->reason,
|
|
|
|
|
|
],
|
|
|
|
|
|
'is_urgent' => false,
|
|
|
|
|
|
'steps' => $steps,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 자동 상신
|
|
|
|
|
|
$approvalService->submit($approval->id);
|
|
|
|
|
|
|
|
|
|
|
|
return $approval->fresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 결재 본문 HTML 생성 (유형별 차별화)
|
2026-02-28 15:54:41 +09:00
|
|
|
|
*/
|
|
|
|
|
|
private function buildLeaveApprovalBody(Leave $leave, int $tenantId): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $leave->user;
|
|
|
|
|
|
$typeName = Leave::TYPE_MAP[$leave->leave_type] ?? $leave->leave_type;
|
2026-03-03 23:50:27 +09:00
|
|
|
|
$formCode = Leave::FORM_CODE_MAP[$leave->leave_type] ?? 'leave';
|
2026-02-28 15:54:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 부서 정보
|
|
|
|
|
|
$profile = $user?->tenantProfiles?->where('tenant_id', $tenantId)->first();
|
|
|
|
|
|
$deptName = $profile?->department?->name ?? '';
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// 공통 행
|
|
|
|
|
|
$rows = [['신청자', e($user->name ?? '')]];
|
2026-02-28 15:54:41 +09:00
|
|
|
|
if ($deptName) {
|
|
|
|
|
|
$rows[] = ['부서', e($deptName)];
|
|
|
|
|
|
}
|
2026-03-03 23:50:27 +09:00
|
|
|
|
$rows[] = ['유형', $typeName];
|
|
|
|
|
|
|
|
|
|
|
|
// 사유서: 대상일 + 사유
|
|
|
|
|
|
if ($formCode === 'reason_report') {
|
|
|
|
|
|
$rows[] = ['대상일', $leave->start_date->format('Y-m-d')];
|
|
|
|
|
|
if ($leave->reason) {
|
|
|
|
|
|
$rows[] = ['사유', e($leave->reason)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$intro = '아래와 같이 사유서를 제출합니다.';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 휴가/근태신청: 기간 + 일수
|
|
|
|
|
|
$period = $leave->start_date->format('Y-m-d').' ~ '.$leave->end_date->format('Y-m-d');
|
|
|
|
|
|
$daysStr = ($leave->days == (int) $leave->days) ? (int) $leave->days.'일' : $leave->days.'일';
|
|
|
|
|
|
$rows[] = ['기간', $period.' ('.$daysStr.')'];
|
|
|
|
|
|
|
|
|
|
|
|
if ($leave->reason) {
|
|
|
|
|
|
$rows[] = ['사유', e($leave->reason)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 잔여연차 (연차 차감 대상만)
|
|
|
|
|
|
if (in_array($leave->leave_type, Leave::DEDUCTIBLE_TYPES)) {
|
|
|
|
|
|
$balance = LeaveBalance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('year', now()->year)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($balance) {
|
|
|
|
|
|
$rows[] = ['잔여연차', $balance->remaining.'일 (부여: '.$balance->total_days.' / 사용: '.$balance->used_days.')'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$intro = match ($formCode) {
|
|
|
|
|
|
'attendance_request' => '아래와 같이 근태를 신청합니다.',
|
|
|
|
|
|
default => '아래와 같이 휴가를 신청합니다.',
|
|
|
|
|
|
};
|
2026-02-28 15:54:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 23:50:27 +09:00
|
|
|
|
// HTML 테이블 생성
|
|
|
|
|
|
$html = "<p>{$intro}</p>";
|
2026-02-28 15:54:41 +09:00
|
|
|
|
$html .= '<table style="border-collapse:collapse; width:100%; margin-top:12px; font-size:14px;">';
|
|
|
|
|
|
$thStyle = 'style="padding:8px 12px; background:#f8f9fa; border:1px solid #dee2e6; text-align:left; width:120px; font-weight:600;"';
|
|
|
|
|
|
$tdStyle = 'style="padding:8px 12px; border:1px solid #dee2e6;"';
|
|
|
|
|
|
foreach ($rows as [$label, $value]) {
|
|
|
|
|
|
$html .= "<tr><th {$thStyle}>{$label}</th><td {$tdStyle}>{$value}</td></tr>";
|
|
|
|
|
|
}
|
|
|
|
|
|
$html .= '</table>';
|
|
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
/**
|
2026-03-03 23:50:27 +09:00
|
|
|
|
* 승인 시 기간 내 영업일마다 Attendance 자동 생성
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
*/
|
2026-03-03 23:50:27 +09:00
|
|
|
|
private function createAttendanceRecords(Leave $leave, int $tenantId, string $status = 'vacation'): void
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
{
|
|
|
|
|
|
$period = CarbonPeriod::create($leave->start_date, $leave->end_date);
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($period as $date) {
|
|
|
|
|
|
if ($date->isWeekend()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Attendance::updateOrCreate(
|
|
|
|
|
|
[
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'user_id' => $leave->user_id,
|
|
|
|
|
|
'base_date' => $date->toDateString(),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
2026-03-03 23:50:27 +09:00
|
|
|
|
'status' => $status,
|
feat: [leave] 휴가관리 Phase 1 구현
- Leave, LeavePolicy, LeaveGrant 모델 생성
- LeaveBalance 헬퍼 메서드 추가 (useLeave, restoreLeave, canUse)
- LeaveService 핵심 로직 (신청, 승인, 반려, 취소, 잔여연차, 통계)
- API 컨트롤러 (목록, 등록, 승인/반려/취소, 잔여연차, 통계, CSV 내보내기)
- 뷰 컨트롤러 + 라우트 등록 (web, api)
- Blade 뷰 (index + 3개 탭 partials: table, balance, stats)
2026-02-26 22:34:31 +09:00
|
|
|
|
'remarks' => $leave->reason ? mb_substr($leave->reason, 0, 100) : null,
|
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 취소 시 해당 기간 vacation Attendance soft delete
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function deleteAttendanceRecords(Leave $leave, int $tenantId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
Attendance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('status', 'vacation')
|
|
|
|
|
|
->whereBetween('base_date', [
|
|
|
|
|
|
$leave->start_date->toDateString(),
|
|
|
|
|
|
$leave->end_date->toDateString(),
|
|
|
|
|
|
])
|
|
|
|
|
|
->update(['deleted_by' => auth()->id()]);
|
|
|
|
|
|
|
|
|
|
|
|
Attendance::query()
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('user_id', $leave->user_id)
|
|
|
|
|
|
->where('status', 'vacation')
|
|
|
|
|
|
->whereBetween('base_date', [
|
|
|
|
|
|
$leave->start_date->toDateString(),
|
|
|
|
|
|
$leave->end_date->toDateString(),
|
|
|
|
|
|
])
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|