feat: [hr] 연차촉진 관리 페이지 추가
- LeavePromotionController: 대상자 목록 조회 + 일괄 통지 발송 - LeaveService: getPromotionCandidates(), sendPromotionNotices() 메서드 추가 - 통지 현황 추적 (미발송/1차 발송/완료) - 일괄 선택 + 결재 문서 자동 생성 + 상신
This commit is contained in:
70
app/Http/Controllers/HR/LeavePromotionController.php
Normal file
70
app/Http/Controllers/HR/LeavePromotionController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\HR;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\HR\LeaveService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class LeavePromotionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private LeaveService $leaveService
|
||||
) {}
|
||||
|
||||
public function index(Request $request): \Illuminate\Contracts\View\View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('hr.leave-promotions.index'));
|
||||
}
|
||||
|
||||
$year = (int) ($request->get('year', now()->year));
|
||||
|
||||
$candidates = $this->leaveService->getPromotionCandidates($year);
|
||||
|
||||
$stats = [
|
||||
'total' => $candidates->count(),
|
||||
'not_sent' => $candidates->where('promotion_status', 'not_sent')->count(),
|
||||
'first_sent' => $candidates->where('promotion_status', 'first_sent')->count(),
|
||||
'completed' => $candidates->where('promotion_status', 'completed')->count(),
|
||||
];
|
||||
|
||||
return view('hr.leave-promotions.index', [
|
||||
'candidates' => $candidates,
|
||||
'stats' => $stats,
|
||||
'year' => $year,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'employee_ids' => 'required|array|min:1',
|
||||
'employee_ids.*' => 'integer',
|
||||
'notice_type' => 'required|in:1st,2nd',
|
||||
'deadline' => 'required_if:notice_type,1st|date',
|
||||
'designated_dates' => 'nullable|array',
|
||||
'designated_dates.*' => 'date',
|
||||
]);
|
||||
|
||||
$year = (int) ($request->get('year', now()->year));
|
||||
$noticeType = $request->get('notice_type');
|
||||
$employeeIds = $request->get('employee_ids');
|
||||
|
||||
$result = $this->leaveService->sendPromotionNotices(
|
||||
employeeIds: $employeeIds,
|
||||
noticeType: $noticeType,
|
||||
year: $year,
|
||||
deadline: $request->get('deadline'),
|
||||
designatedDates: $request->get('designated_dates', []),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => count($result['created']).'건의 연차촉진 통지서가 생성되었습니다.',
|
||||
'created' => $result['created'],
|
||||
'skipped' => $result['skipped'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -803,6 +803,188 @@ public function getActiveEmployees(): \Illuminate\Database\Eloquent\Collection
|
||||
->get(['id', 'user_id', 'display_name', 'department_id']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 연차촉진 관리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 연차촉진 대상자 조회 — 잔여 연차가 있는 직원 + 통지 발송 현황
|
||||
*/
|
||||
public function getPromotionCandidates(int $year): Collection
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 1. 연차 현황 조회 (잔여 연차 > 0인 직원)
|
||||
$balances = $this->getBalanceSummary($year, 'name', 'asc', 'active');
|
||||
|
||||
// 2. 기존 발송된 통지서 조회 (approvals 테이블)
|
||||
$promotionApprovals = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereHas('form', function ($q) {
|
||||
$q->whereIn('code', ['leave_promotion_1st', 'leave_promotion_2nd']);
|
||||
})
|
||||
->with('form:id,code')
|
||||
->get(['id', 'form_id', 'content', 'status', 'created_at']);
|
||||
|
||||
// 3. employee_id 기준으로 발송 현황 매핑
|
||||
$sentMap = [];
|
||||
foreach ($promotionApprovals as $approval) {
|
||||
$employeeId = $approval->content['employee_id'] ?? null;
|
||||
$noticeYear = $approval->content['year'] ?? null;
|
||||
if (! $employeeId || $noticeYear != $year) {
|
||||
continue;
|
||||
}
|
||||
$code = $approval->form?->code;
|
||||
if (! isset($sentMap[$employeeId])) {
|
||||
$sentMap[$employeeId] = ['1st' => null, '2nd' => null];
|
||||
}
|
||||
if ($code === 'leave_promotion_1st') {
|
||||
$sentMap[$employeeId]['1st'] = $approval;
|
||||
} elseif ($code === 'leave_promotion_2nd') {
|
||||
$sentMap[$employeeId]['2nd'] = $approval;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 잔여일수 > 0 필터링 + 통지 상태 결합
|
||||
return $balances
|
||||
->filter(fn ($b) => ($b->total_days - $b->used_days) > 0)
|
||||
->map(function ($balance) use ($sentMap) {
|
||||
$userId = $balance->user_id;
|
||||
$sent = $sentMap[$userId] ?? ['1st' => null, '2nd' => null];
|
||||
|
||||
$balance->first_notice = $sent['1st'];
|
||||
$balance->second_notice = $sent['2nd'];
|
||||
$balance->promotion_status = match (true) {
|
||||
$sent['2nd'] !== null => 'completed',
|
||||
$sent['1st'] !== null => 'first_sent',
|
||||
default => 'not_sent',
|
||||
};
|
||||
|
||||
return $balance;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* 연차촉진 통지서 일괄 생성 (결재 문서로 생성 + 자동 상신)
|
||||
*/
|
||||
public function sendPromotionNotices(array $employeeIds, string $noticeType, int $year, ?string $deadline = null, array $designatedDates = []): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$formCode = $noticeType === '1st' ? 'leave_promotion_1st' : 'leave_promotion_2nd';
|
||||
|
||||
$form = ApprovalForm::where('code', $formCode)
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (! $form) {
|
||||
throw new \RuntimeException('연차촉진 통지서 결재 양식이 등록되지 않았습니다.');
|
||||
}
|
||||
|
||||
// 기본 결재선
|
||||
$line = ApprovalLine::where('tenant_id', $tenantId)
|
||||
->where('is_default', true)
|
||||
->first();
|
||||
|
||||
if (! $line) {
|
||||
throw new \RuntimeException('기본 결재선을 설정해주세요.');
|
||||
}
|
||||
|
||||
// 대상 직원 + 잔여연차 조회
|
||||
$balances = LeaveBalance::query()
|
||||
->forTenant($tenantId)
|
||||
->forYear($year)
|
||||
->whereIn('user_id', $employeeIds)
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
$employees = Employee::query()
|
||||
->with(['user:id,name', 'department:id,name'])
|
||||
->forTenant($tenantId)
|
||||
->whereIn('user_id', $employeeIds)
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
// 이미 발송된 직원 확인
|
||||
$existingApprovals = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('form_id', $form->id)
|
||||
->get(['id', 'content'])
|
||||
->filter(function ($a) use ($year) {
|
||||
return ($a->content['year'] ?? null) == $year;
|
||||
})
|
||||
->pluck('content.employee_id')
|
||||
->filter()
|
||||
->toArray();
|
||||
|
||||
$approvalService = app(\App\Services\ApprovalService::class);
|
||||
$steps = collect($line->steps)->map(fn ($s) => [
|
||||
'user_id' => $s['user_id'],
|
||||
'step_type' => $s['step_type'] ?? $s['type'] ?? 'approval',
|
||||
])->toArray();
|
||||
|
||||
$created = [];
|
||||
$skipped = [];
|
||||
|
||||
foreach ($employeeIds as $userId) {
|
||||
// 이미 발송된 직원 스킵
|
||||
if (in_array($userId, $existingApprovals)) {
|
||||
$skipped[] = $userId;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$employee = $employees->get($userId);
|
||||
$balance = $balances->get($userId);
|
||||
|
||||
if (! $employee || ! $balance) {
|
||||
$skipped[] = $userId;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$remaining = $balance->total_days - $balance->used_days;
|
||||
$empName = $employee->display_name ?? $employee->user?->name ?? '';
|
||||
$deptName = $employee->department?->name ?? '';
|
||||
|
||||
// 통지서 content 구성
|
||||
$content = [
|
||||
'employee_id' => $userId,
|
||||
'employee_name' => $empName,
|
||||
'department' => $deptName,
|
||||
'position' => $employee->position_key ?? '',
|
||||
'year' => $year,
|
||||
'total_days' => $balance->total_days,
|
||||
'used_days' => $balance->used_days,
|
||||
'remaining_days' => $remaining,
|
||||
];
|
||||
|
||||
if ($noticeType === '1st') {
|
||||
$content['deadline'] = $deadline;
|
||||
$title = "연차촉진 1차 통지 - {$empName} ({$year}년 잔여 {$remaining}일)";
|
||||
} else {
|
||||
$content['designated_dates'] = $designatedDates;
|
||||
$title = "연차촉진 2차 통지 - {$empName} ({$year}년 잔여 {$remaining}일)";
|
||||
}
|
||||
|
||||
$approval = $approvalService->createApproval([
|
||||
'form_id' => $form->id,
|
||||
'line_id' => $line->id,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'body' => '',
|
||||
'is_urgent' => false,
|
||||
'steps' => $steps,
|
||||
]);
|
||||
|
||||
$approvalService->submit($approval->id);
|
||||
$created[] = $userId;
|
||||
}
|
||||
|
||||
return ['created' => $created, 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user