feat: [approval] 사직서 양식 추가
- ResignationService 생성 (정보 조회 + PDF 생성) - 사직서 전용 폼/조회 파셜 추가 - create/show 블레이드에 사직서 JS 로직 통합 - 컨트롤러 resignationInfo/resignationPdf 메서드 추가 - API 라우트 resignation-info, resignation-pdf 등록
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
use App\Services\CareerCertService;
|
use App\Services\CareerCertService;
|
||||||
use App\Services\EmploymentCertService;
|
use App\Services\EmploymentCertService;
|
||||||
use App\Services\GoogleCloudStorageService;
|
use App\Services\GoogleCloudStorageService;
|
||||||
|
use App\Services\ResignationService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -368,6 +369,43 @@ public function appointmentCertPdf(int $id)
|
|||||||
return $service->generatePdfResponse($content);
|
return $service->generatePdfResponse($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 사직서
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사원 사직서 정보 조회
|
||||||
|
*/
|
||||||
|
public function resignationInfo(int $userId): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tenantId = session('selected_tenant_id');
|
||||||
|
$service = app(ResignationService::class);
|
||||||
|
$data = $service->getCertInfo($userId, $tenantId);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'data' => $data]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '사원 정보를 불러올 수 없습니다.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사직서 PDF 다운로드
|
||||||
|
*/
|
||||||
|
public function resignationPdf(int $id)
|
||||||
|
{
|
||||||
|
$approval = \App\Models\Approvals\Approval::where('tenant_id', session('selected_tenant_id'))
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$content = $approval->content ?? [];
|
||||||
|
$service = app(ResignationService::class);
|
||||||
|
|
||||||
|
return $service->generatePdfResponse($content);
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 워크플로우
|
// 워크플로우
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
201
app/Services/ResignationService.php
Normal file
201
app/Services/ResignationService.php
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\HR\Employee;
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use App\Models\Tenants\TenantSetting;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ResignationService
|
||||||
|
{
|
||||||
|
private ?string $koreanFontName = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
if (! defined('K_PATH_FONTS')) {
|
||||||
|
$tcpdfFontsDir = dirname(__DIR__, 2).'/storage/fonts/tcpdf/';
|
||||||
|
if (is_dir($tcpdfFontsDir)) {
|
||||||
|
define('K_PATH_FONTS', $tcpdfFontsDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사원의 사직서 정보 조회
|
||||||
|
*/
|
||||||
|
public function getCertInfo(int $userId, int $tenantId): array
|
||||||
|
{
|
||||||
|
$employee = Employee::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->with(['user', 'department'])
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$tenant = Tenant::findOrFail($tenantId);
|
||||||
|
|
||||||
|
$displaySetting = TenantSetting::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('setting_group', 'company')
|
||||||
|
->where('setting_key', 'display_company_name')
|
||||||
|
->first();
|
||||||
|
$displayName = $displaySetting?->setting_value ?? '';
|
||||||
|
$companyName = ! empty($displayName) ? $displayName : ($tenant->company_name ?? '');
|
||||||
|
|
||||||
|
// 주민등록번호 마스킹
|
||||||
|
$residentNumber = $employee->resident_number ?? '';
|
||||||
|
$maskedResident = '';
|
||||||
|
if (strlen($residentNumber) >= 8) {
|
||||||
|
$front = substr($residentNumber, 0, 6);
|
||||||
|
$back = substr($residentNumber, 7, 1);
|
||||||
|
$maskedResident = $front.'-'.$back.'******';
|
||||||
|
} elseif (! empty($residentNumber)) {
|
||||||
|
$maskedResident = $residentNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $employee->user->name ?? $employee->display_name ?? '',
|
||||||
|
'resident_number' => $maskedResident,
|
||||||
|
'department' => $employee->department?->name ?? '',
|
||||||
|
'position' => $employee->position_label ?? '',
|
||||||
|
'hire_date' => $employee->hire_date ?? '',
|
||||||
|
'address' => $employee->address ?? '',
|
||||||
|
'company_name' => $companyName,
|
||||||
|
'ceo_name' => $tenant->ceo_name ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* content JSON 기반 PDF Response 생성
|
||||||
|
*/
|
||||||
|
public function generatePdfResponse(array $content): \Illuminate\Http\Response
|
||||||
|
{
|
||||||
|
$pdf = new \TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||||||
|
$pdf->SetCreator('SAM');
|
||||||
|
$pdf->SetAuthor($content['company_name'] ?? 'SAM');
|
||||||
|
$pdf->SetTitle('사직서');
|
||||||
|
|
||||||
|
$pdf->setPrintHeader(false);
|
||||||
|
$pdf->setPrintFooter(false);
|
||||||
|
$pdf->SetMargins(20, 20, 20);
|
||||||
|
$pdf->SetAutoPageBreak(true, 20);
|
||||||
|
|
||||||
|
$font = $this->getKoreanFont();
|
||||||
|
|
||||||
|
$pdf->AddPage();
|
||||||
|
|
||||||
|
// 제목
|
||||||
|
$pdf->SetFont($font, 'B', 22);
|
||||||
|
$pdf->Cell(0, 20, '사 직 서', 0, 1, 'C');
|
||||||
|
$pdf->Ln(8);
|
||||||
|
|
||||||
|
// 테이블
|
||||||
|
$this->addTableRow($pdf, $font, [
|
||||||
|
['소 속', $content['department'] ?? '-', 40],
|
||||||
|
['직 위', $content['position'] ?? '-', 40],
|
||||||
|
]);
|
||||||
|
$this->addTableRow($pdf, $font, [
|
||||||
|
['성 명', $content['name'] ?? '-', 40],
|
||||||
|
['주민등록번호', $content['resident_number'] ?? '-', 40],
|
||||||
|
]);
|
||||||
|
$this->addTableRow($pdf, $font, [
|
||||||
|
['입사일', $content['hire_date'] ?? '-', 40],
|
||||||
|
['퇴사(예정)일', $content['resign_date'] ?? '-', 40],
|
||||||
|
]);
|
||||||
|
$this->addTableRow($pdf, $font, [
|
||||||
|
['주 소', $content['address'] ?? '-', 0],
|
||||||
|
]);
|
||||||
|
$this->addTableRow($pdf, $font, [
|
||||||
|
['사 유', $content['reason'] ?? '-', 0],
|
||||||
|
]);
|
||||||
|
$pdf->Ln(12);
|
||||||
|
|
||||||
|
// 증명 문구
|
||||||
|
$pdf->SetFont($font, '', 12);
|
||||||
|
$pdf->MultiCell(0, 8, '상기 본인은 위 사유로 인하여 사직하고자'."\n".'이에 사직서를 제출하오니 허가하여 주시기 바랍니다.', 0, 'C');
|
||||||
|
$pdf->Ln(6);
|
||||||
|
|
||||||
|
// 날짜
|
||||||
|
$issueDate = $content['issue_date'] ?? date('Y-m-d');
|
||||||
|
$issueDateFormatted = $this->formatDate($issueDate);
|
||||||
|
$pdf->SetFont($font, 'B', 12);
|
||||||
|
$pdf->Cell(0, 10, $issueDateFormatted, 0, 1, 'C');
|
||||||
|
$pdf->Ln(6);
|
||||||
|
|
||||||
|
// 신청인
|
||||||
|
$pdf->SetFont($font, '', 12);
|
||||||
|
$pdf->Cell(0, 10, '신청인 '.($content['name'] ?? '').' (인)', 0, 1, 'C');
|
||||||
|
$pdf->Ln(12);
|
||||||
|
|
||||||
|
// 회사명 + 대표이사 귀하
|
||||||
|
$ceoName = $content['ceo_name'] ?? '';
|
||||||
|
$pdf->SetFont($font, 'B', 14);
|
||||||
|
$pdf->Cell(0, 10, ($content['company_name'] ?? '').' 대표이사 귀하', 0, 1, 'C');
|
||||||
|
|
||||||
|
$pdfContent = $pdf->Output('', 'S');
|
||||||
|
$fileName = '사직서_'.($content['name'] ?? '').'.pdf';
|
||||||
|
|
||||||
|
return response($pdfContent, 200, [
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
'Content-Disposition' => 'inline; filename="'.$fileName.'"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addTableRow(\TCPDF $pdf, string $font, array $cells): void
|
||||||
|
{
|
||||||
|
$pageWidth = $pdf->getPageWidth() - 40;
|
||||||
|
$rowHeight = 8;
|
||||||
|
$thWidth = 30;
|
||||||
|
|
||||||
|
if (count($cells) === 1) {
|
||||||
|
$pdf->SetFont($font, 'B', 10);
|
||||||
|
$pdf->SetFillColor(248, 249, 250);
|
||||||
|
$pdf->Cell($thWidth, $rowHeight, $cells[0][0], 1, 0, 'L', true);
|
||||||
|
$pdf->SetFont($font, '', 10);
|
||||||
|
$pdf->Cell($pageWidth - $thWidth, $rowHeight, $cells[0][1], 1, 1, 'L');
|
||||||
|
} else {
|
||||||
|
$tdWidth = ($pageWidth - $thWidth * 2) / 2;
|
||||||
|
foreach ($cells as $cell) {
|
||||||
|
$pdf->SetFont($font, 'B', 10);
|
||||||
|
$pdf->SetFillColor(248, 249, 250);
|
||||||
|
$pdf->Cell($thWidth, $rowHeight, $cell[0], 1, 0, 'L', true);
|
||||||
|
$pdf->SetFont($font, '', 10);
|
||||||
|
$pdf->Cell($tdWidth, $rowHeight, $cell[1], 1, 0, 'L');
|
||||||
|
}
|
||||||
|
$pdf->Ln();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDate(string $date): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return date('Y년 m월 d일', strtotime($date));
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getKoreanFont(): string
|
||||||
|
{
|
||||||
|
if ($this->koreanFontName) {
|
||||||
|
return $this->koreanFontName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined('K_PATH_FONTS') && file_exists(K_PATH_FONTS.'pretendard.php')) {
|
||||||
|
$this->koreanFontName = 'pretendard';
|
||||||
|
|
||||||
|
return $this->koreanFontName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fontPath = storage_path('fonts/Pretendard-Regular.ttf');
|
||||||
|
if (file_exists($fontPath)) {
|
||||||
|
try {
|
||||||
|
$this->koreanFontName = \TCPDF_FONTS::addTTFfont($fontPath, 'TrueTypeUnicode', '', 96);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('TCPDF 한글 폰트 등록 실패', ['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->koreanFontName ?: 'helvetica';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
|||||||
'employees' => $employees ?? collect(),
|
'employees' => $employees ?? collect(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
{{-- 사직서 전용 폼 --}}
|
||||||
|
@include('approvals.partials._resignation-form', [
|
||||||
|
'employees' => $employees ?? collect(),
|
||||||
|
])
|
||||||
|
|
||||||
{{-- 지출결의서 전용 폼 --}}
|
{{-- 지출결의서 전용 폼 --}}
|
||||||
@include('approvals.partials._expense-form', [
|
@include('approvals.partials._expense-form', [
|
||||||
'initialData' => [],
|
'initialData' => [],
|
||||||
@@ -235,6 +240,7 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
|
|||||||
let isCertForm = false;
|
let isCertForm = false;
|
||||||
let isCareerCertForm = false;
|
let isCareerCertForm = false;
|
||||||
let isAppointmentCertForm = false;
|
let isAppointmentCertForm = false;
|
||||||
|
let isResignationForm = false;
|
||||||
|
|
||||||
// 양식코드별 표시할 유형 목록
|
// 양식코드별 표시할 유형 목록
|
||||||
const leaveTypesByFormCode = {
|
const leaveTypesByFormCode = {
|
||||||
@@ -480,6 +486,7 @@ function switchFormMode(formId) {
|
|||||||
const certContainer = document.getElementById('cert-form-container');
|
const certContainer = document.getElementById('cert-form-container');
|
||||||
const careerCertContainer = document.getElementById('career-cert-form-container');
|
const careerCertContainer = document.getElementById('career-cert-form-container');
|
||||||
const appointmentCertContainer = document.getElementById('appointment-cert-form-container');
|
const appointmentCertContainer = document.getElementById('appointment-cert-form-container');
|
||||||
|
const resignationContainer = document.getElementById('resignation-form-container');
|
||||||
const bodyArea = document.getElementById('body-area');
|
const bodyArea = document.getElementById('body-area');
|
||||||
const expenseLoadBtn = document.getElementById('expense-load-btn');
|
const expenseLoadBtn = document.getElementById('expense-load-btn');
|
||||||
|
|
||||||
@@ -491,6 +498,7 @@ function switchFormMode(formId) {
|
|||||||
certContainer.style.display = 'none';
|
certContainer.style.display = 'none';
|
||||||
careerCertContainer.style.display = 'none';
|
careerCertContainer.style.display = 'none';
|
||||||
appointmentCertContainer.style.display = 'none';
|
appointmentCertContainer.style.display = 'none';
|
||||||
|
resignationContainer.style.display = 'none';
|
||||||
expenseLoadBtn.style.display = 'none';
|
expenseLoadBtn.style.display = 'none';
|
||||||
bodyArea.style.display = 'none';
|
bodyArea.style.display = 'none';
|
||||||
isExpenseForm = false;
|
isExpenseForm = false;
|
||||||
@@ -498,6 +506,7 @@ function switchFormMode(formId) {
|
|||||||
isCertForm = false;
|
isCertForm = false;
|
||||||
isCareerCertForm = false;
|
isCareerCertForm = false;
|
||||||
isAppointmentCertForm = false;
|
isAppointmentCertForm = false;
|
||||||
|
isResignationForm = false;
|
||||||
|
|
||||||
if (code === 'expense') {
|
if (code === 'expense') {
|
||||||
isExpenseForm = true;
|
isExpenseForm = true;
|
||||||
@@ -528,6 +537,11 @@ function switchFormMode(formId) {
|
|||||||
appointmentCertContainer.style.display = '';
|
appointmentCertContainer.style.display = '';
|
||||||
const acUserId = document.getElementById('ac-user-id').value;
|
const acUserId = document.getElementById('ac-user-id').value;
|
||||||
if (acUserId) loadAppointmentCertInfo(acUserId);
|
if (acUserId) loadAppointmentCertInfo(acUserId);
|
||||||
|
} else if (code === 'resignation') {
|
||||||
|
isResignationForm = true;
|
||||||
|
resignationContainer.style.display = '';
|
||||||
|
const rgUserId = document.getElementById('rg-user-id').value;
|
||||||
|
if (rgUserId) loadResignationInfo(rgUserId);
|
||||||
} else {
|
} else {
|
||||||
bodyArea.style.display = '';
|
bodyArea.style.display = '';
|
||||||
}
|
}
|
||||||
@@ -538,7 +552,7 @@ function applyBodyTemplate(formId) {
|
|||||||
switchFormMode(formId);
|
switchFormMode(formId);
|
||||||
|
|
||||||
// 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀
|
// 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀
|
||||||
if (isExpenseForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm) {
|
if (isExpenseForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm) {
|
||||||
const titleEl = document.getElementById('title');
|
const titleEl = document.getElementById('title');
|
||||||
const formSelect = document.getElementById('form_id');
|
const formSelect = document.getElementById('form_id');
|
||||||
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
|
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
|
||||||
@@ -712,6 +726,33 @@ function applyBodyTemplate(formId) {
|
|||||||
issue_date: document.getElementById('ac-issue-date').value,
|
issue_date: document.getElementById('ac-issue-date').value,
|
||||||
};
|
};
|
||||||
formBody = null;
|
formBody = null;
|
||||||
|
} else if (isResignationForm) {
|
||||||
|
const reason = getResignationReason();
|
||||||
|
if (!reason) {
|
||||||
|
showToast('사직사유를 입력해주세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resignDate = document.getElementById('rg-resign-date').value;
|
||||||
|
if (!resignDate) {
|
||||||
|
showToast('퇴사(예정)일을 입력해주세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formContent = {
|
||||||
|
cert_user_id: document.getElementById('rg-user-id').value,
|
||||||
|
name: document.getElementById('rg-name').value,
|
||||||
|
resident_number: document.getElementById('rg-resident').value,
|
||||||
|
department: document.getElementById('rg-department').value,
|
||||||
|
position: document.getElementById('rg-position').value,
|
||||||
|
hire_date: document.getElementById('rg-hire-date').value,
|
||||||
|
resign_date: resignDate,
|
||||||
|
address: document.getElementById('rg-address').value,
|
||||||
|
reason: reason,
|
||||||
|
company_name: document.getElementById('rg-company-name').value,
|
||||||
|
ceo_name: document.getElementById('rg-ceo-name').value,
|
||||||
|
issue_date: document.getElementById('rg-issue-date').value,
|
||||||
|
};
|
||||||
|
formBody = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -1294,6 +1335,150 @@ function buildAppointmentCertPreviewHtml(d) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 사직서 관련 함수
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
async function loadResignationInfo(userId) {
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/admin/approvals/resignation-info/${userId}`, {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const d = data.data;
|
||||||
|
document.getElementById('rg-name').value = d.name || '';
|
||||||
|
document.getElementById('rg-resident').value = d.resident_number || '';
|
||||||
|
document.getElementById('rg-department').value = d.department || '';
|
||||||
|
document.getElementById('rg-position').value = d.position || '';
|
||||||
|
document.getElementById('rg-hire-date').value = d.hire_date || '';
|
||||||
|
document.getElementById('rg-address').value = d.address || '';
|
||||||
|
document.getElementById('rg-company-name').value = d.company_name || '';
|
||||||
|
document.getElementById('rg-ceo-name').value = d.ceo_name || '';
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResignationReasonChange() {
|
||||||
|
const sel = document.getElementById('rg-reason-select');
|
||||||
|
const customWrap = document.getElementById('rg-reason-custom-wrap');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
customWrap.style.display = '';
|
||||||
|
document.getElementById('rg-reason-custom').focus();
|
||||||
|
} else {
|
||||||
|
customWrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResignationReason() {
|
||||||
|
const sel = document.getElementById('rg-reason-select');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
return document.getElementById('rg-reason-custom').value.trim();
|
||||||
|
}
|
||||||
|
return sel.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResignationPreview() {
|
||||||
|
const department = document.getElementById('rg-department').value || '-';
|
||||||
|
const position = document.getElementById('rg-position').value || '-';
|
||||||
|
const name = document.getElementById('rg-name').value || '-';
|
||||||
|
const resident = document.getElementById('rg-resident').value || '-';
|
||||||
|
const hireDate = document.getElementById('rg-hire-date').value || '-';
|
||||||
|
const resignDate = document.getElementById('rg-resign-date').value || '-';
|
||||||
|
const address = document.getElementById('rg-address').value || '-';
|
||||||
|
const reason = getResignationReason() || '-';
|
||||||
|
const issueDate = document.getElementById('rg-issue-date').value || '-';
|
||||||
|
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
|
||||||
|
const company = document.getElementById('rg-company-name').value || '-';
|
||||||
|
const ceoName = document.getElementById('rg-ceo-name').value || '-';
|
||||||
|
|
||||||
|
const el = document.getElementById('resignation-preview-content');
|
||||||
|
el.innerHTML = buildResignationPreviewHtml({ department, position, name, resident, hireDate, resignDate, address, reason, issueDateFormatted, company, ceoName });
|
||||||
|
|
||||||
|
document.getElementById('resignation-preview-modal').style.display = '';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResignationPreview() {
|
||||||
|
document.getElementById('resignation-preview-modal').style.display = 'none';
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function printResignationPreview() {
|
||||||
|
const content = document.getElementById('resignation-preview-content').innerHTML;
|
||||||
|
const win = window.open('', '_blank', 'width=800,height=1000');
|
||||||
|
win.document.write('<html><head><title>사직서</title>');
|
||||||
|
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
|
||||||
|
win.document.write('</head><body>');
|
||||||
|
win.document.write(content);
|
||||||
|
win.document.write('</body></html>');
|
||||||
|
win.document.close();
|
||||||
|
win.onload = function() { win.print(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResignationPreviewHtml(d) {
|
||||||
|
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
|
||||||
|
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
|
||||||
|
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">사 직 서</h1>
|
||||||
|
|
||||||
|
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
|
||||||
|
<tr>
|
||||||
|
<th style="${thStyle}">소 속</th>
|
||||||
|
<td style="${tdStyle}">${e(d.department)}</td>
|
||||||
|
<th style="${thStyle}">직 위</th>
|
||||||
|
<td style="${tdStyle}">${e(d.position)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="${thStyle}">성 명</th>
|
||||||
|
<td style="${tdStyle}">${e(d.name)}</td>
|
||||||
|
<th style="${thStyle}">주민등록번호</th>
|
||||||
|
<td style="${tdStyle}">${e(d.resident)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="${thStyle}">입사일</th>
|
||||||
|
<td style="${tdStyle}">${e(d.hireDate)}</td>
|
||||||
|
<th style="${thStyle}">퇴사(예정)일</th>
|
||||||
|
<td style="${tdStyle}">${e(d.resignDate)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="${thStyle}">주 소</th>
|
||||||
|
<td style="${tdStyle}" colspan="3">${e(d.address)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="${thStyle}">사 유</th>
|
||||||
|
<td style="${tdStyle}" colspan="3">${e(d.reason)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="text-align:center; font-size:15px; line-height:1.8; margin:36px 0;">
|
||||||
|
상기 본인은 위 사유로 인하여 사직하고자<br>
|
||||||
|
이에 사직서를 제출하오니 허가하여 주시기 바랍니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:24px;">
|
||||||
|
${e(d.issueDateFormatted)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="text-align:center; font-size:14px; margin-bottom:48px;">
|
||||||
|
신청인 ${e(d.name)} (인)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="text-align:center; margin-top:32px;">
|
||||||
|
<p style="font-size:16px; font-weight:600;">${e(d.company)} 대표이사 귀하</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildCertPreviewHtml(d) {
|
function buildCertPreviewHtml(d) {
|
||||||
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
|
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
|
||||||
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
|
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
|
||||||
|
|||||||
156
resources/views/approvals/partials/_resignation-form.blade.php
Normal file
156
resources/views/approvals/partials/_resignation-form.blade.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{{--
|
||||||
|
사직서 전용 폼
|
||||||
|
Props:
|
||||||
|
$employees (Collection) - 활성 사원 목록
|
||||||
|
--}}
|
||||||
|
@php
|
||||||
|
$employees = $employees ?? collect();
|
||||||
|
$currentUserId = auth()->id();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div id="resignation-form-container" style="display: none;" class="mb-4">
|
||||||
|
<input type="hidden" id="rg-company-name" value="">
|
||||||
|
<input type="hidden" id="rg-ceo-name" value="">
|
||||||
|
<div class="space-y-4">
|
||||||
|
{{-- 대상 사원 --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">대상 사원 <span class="text-red-500">*</span></label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<select id="rg-user-id" onchange="loadResignationInfo(this.value)"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
style="max-width: 300px;">
|
||||||
|
@foreach($employees as $emp)
|
||||||
|
<option value="{{ $emp->user_id }}" {{ $emp->user_id == $currentUserId ? 'selected' : '' }}>
|
||||||
|
{{ $emp->display_name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<button type="button" onclick="openResignationPreview()"
|
||||||
|
class="shrink-0 px-3 py-2 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg>
|
||||||
|
미리보기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 인적사항 --}}
|
||||||
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">인적사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">소속</label>
|
||||||
|
<input type="text" id="rg-department" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">직위</label>
|
||||||
|
<input type="text" id="rg-position" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">성명</label>
|
||||||
|
<input type="text" id="rg-name" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">주민등록번호</label>
|
||||||
|
<input type="text" id="rg-resident" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 200px; max-width: 200px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">입사일</label>
|
||||||
|
<input type="text" id="rg-hire-date" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1 1 200px; max-width: 200px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">퇴사(예정)일 <span class="text-red-500">*</span></label>
|
||||||
|
<input type="date" id="rg-resign-date"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">주소 <span class="text-blue-500 text-xs">(수정 가능)</span></label>
|
||||||
|
<input type="text" id="rg-address"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 사직사유 --}}
|
||||||
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">사직사유</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 250px; max-width: 300px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">사유 선택 <span class="text-red-500">*</span></label>
|
||||||
|
<select id="rg-reason-select" onchange="onResignationReasonChange()"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="일신상의 사유">일신상의 사유</option>
|
||||||
|
<option value="가사 사정">가사 사정</option>
|
||||||
|
<option value="건강상의 이유">건강상의 이유</option>
|
||||||
|
<option value="진학/학업">진학/학업</option>
|
||||||
|
<option value="이직">이직</option>
|
||||||
|
<option value="__custom__">기타 (직접입력)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="rg-reason-custom-wrap" style="flex: 1 1 250px; max-width: 300px; display: none;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">직접입력</label>
|
||||||
|
<input type="text" id="rg-reason-custom" placeholder="사유를 입력하세요"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 제출일 --}}
|
||||||
|
<div style="max-width: 200px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">제출일</label>
|
||||||
|
<input type="text" id="rg-issue-date" readonly
|
||||||
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700"
|
||||||
|
value="{{ now()->format('Y-m-d') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 사직서 미리보기 모달 --}}
|
||||||
|
<div id="resignation-preview-modal" style="display: none;" class="fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/50" onclick="closeResignationPreview()"></div>
|
||||||
|
<div class="relative flex items-center justify-center min-h-full p-4">
|
||||||
|
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 700px;">
|
||||||
|
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
|
<h3 class="text-base font-semibold text-gray-800">사직서 미리보기</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" onclick="printResignationPreview()"
|
||||||
|
class="px-3 py-1.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg text-xs font-medium transition inline-flex items-center gap-1">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||||
|
</svg>
|
||||||
|
인쇄
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="closeResignationPreview()"
|
||||||
|
class="p-1 text-gray-400 hover:text-gray-600 transition">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-auto" style="max-height: 80vh;">
|
||||||
|
<div id="resignation-preview-content" style="padding: 48px 56px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
105
resources/views/approvals/partials/_resignation-show.blade.php
Normal file
105
resources/views/approvals/partials/_resignation-show.blade.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{{--
|
||||||
|
사직서 읽기전용 렌더링
|
||||||
|
Props:
|
||||||
|
$content (array) - approvals.content JSON
|
||||||
|
--}}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{{-- 미리보기 / PDF 다운로드 버튼 --}}
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" onclick="openResignationShowPreview()"
|
||||||
|
class="px-3 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg>
|
||||||
|
사직서 미리보기
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('api.admin.approvals.resignation-pdf', $approval->id) }}" target="_blank"
|
||||||
|
class="px-3 py-1.5 bg-green-50 text-green-600 hover:bg-green-100 border border-green-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
PDF 다운로드
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 인적사항 --}}
|
||||||
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">인적사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">소속</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['department'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">직위</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['position'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">성명</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['name'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">주민등록번호</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['resident_number'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">입사일</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['hire_date'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">퇴사(예정)일</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['resign_date'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1 / -1;">
|
||||||
|
<span class="text-xs text-gray-500">주소</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['address'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 사직사유 --}}
|
||||||
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">사직사유</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="text-sm font-medium">{{ $content['reason'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 미리보기 모달 --}}
|
||||||
|
<div id="resignation-show-preview-modal" style="display: none;" class="fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/50" onclick="closeResignationShowPreview()"></div>
|
||||||
|
<div class="relative flex items-center justify-center min-h-full p-4">
|
||||||
|
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 700px;">
|
||||||
|
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
|
<h3 class="text-base font-semibold text-gray-800">사직서 미리보기</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" onclick="printResignationShowPreview()"
|
||||||
|
class="px-3 py-1.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg text-xs font-medium transition inline-flex items-center gap-1">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||||
|
</svg>
|
||||||
|
인쇄
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="closeResignationShowPreview()"
|
||||||
|
class="p-1 text-gray-400 hover:text-gray-600 transition">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-auto" style="max-height: 80vh;">
|
||||||
|
<div id="resignation-show-preview-content" style="padding: 48px 56px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -88,6 +88,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
|
|||||||
@include('approvals.partials._career-cert-show', ['content' => $approval->content])
|
@include('approvals.partials._career-cert-show', ['content' => $approval->content])
|
||||||
@elseif(!empty($approval->content) && $approval->form?->code === 'appointment_cert')
|
@elseif(!empty($approval->content) && $approval->form?->code === 'appointment_cert')
|
||||||
@include('approvals.partials._appointment-cert-show', ['content' => $approval->content])
|
@include('approvals.partials._appointment-cert-show', ['content' => $approval->content])
|
||||||
|
@elseif(!empty($approval->content) && $approval->form?->code === 'resignation')
|
||||||
|
@include('approvals.partials._resignation-show', ['content' => $approval->content])
|
||||||
@elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
|
@elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
|
||||||
<div class="prose prose-sm max-w-none text-gray-700">
|
<div class="prose prose-sm max-w-none text-gray-700">
|
||||||
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}
|
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}
|
||||||
@@ -493,6 +495,74 @@ class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition te
|
|||||||
// 재직증명서 미리보기 (show 페이지용)
|
// 재직증명서 미리보기 (show 페이지용)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
@if(!empty($approval->content) && $approval->form?->code === 'resignation')
|
||||||
|
const _resignationContent = @json($approval->content);
|
||||||
|
|
||||||
|
function openResignationShowPreview() {
|
||||||
|
const c = _resignationContent;
|
||||||
|
const issueDate = c.issue_date || '';
|
||||||
|
const issueDateFormatted = issueDate ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
|
||||||
|
|
||||||
|
const el = document.getElementById('resignation-show-preview-content');
|
||||||
|
el.innerHTML = _buildResignationHtml({
|
||||||
|
department: c.department || '-',
|
||||||
|
position: c.position || '-',
|
||||||
|
name: c.name || '-',
|
||||||
|
resident: c.resident_number || '-',
|
||||||
|
hireDate: c.hire_date || '-',
|
||||||
|
resignDate: c.resign_date || '-',
|
||||||
|
address: c.address || '-',
|
||||||
|
reason: c.reason || '-',
|
||||||
|
issueDateFormatted: issueDateFormatted,
|
||||||
|
company: c.company_name || '-',
|
||||||
|
ceoName: c.ceo_name || '-',
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('resignation-show-preview-modal').style.display = '';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResignationShowPreview() {
|
||||||
|
document.getElementById('resignation-show-preview-modal').style.display = 'none';
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function printResignationShowPreview() {
|
||||||
|
const content = document.getElementById('resignation-show-preview-content').innerHTML;
|
||||||
|
const win = window.open('', '_blank', 'width=800,height=1000');
|
||||||
|
win.document.write('<html><head><title>사직서</title>');
|
||||||
|
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
|
||||||
|
win.document.write('</head><body>');
|
||||||
|
win.document.write(content);
|
||||||
|
win.document.write('</body></html>');
|
||||||
|
win.document.close();
|
||||||
|
win.onload = function() { win.print(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildResignationHtml(d) {
|
||||||
|
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
|
||||||
|
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
|
||||||
|
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">사 직 서</h1>
|
||||||
|
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
|
||||||
|
<tr><th style="${thStyle}">소 속</th><td style="${tdStyle}">${e(d.department)}</td><th style="${thStyle}">직 위</th><td style="${tdStyle}">${e(d.position)}</td></tr>
|
||||||
|
<tr><th style="${thStyle}">성 명</th><td style="${tdStyle}">${e(d.name)}</td><th style="${thStyle}">주민등록번호</th><td style="${tdStyle}">${e(d.resident)}</td></tr>
|
||||||
|
<tr><th style="${thStyle}">입사일</th><td style="${tdStyle}">${e(d.hireDate)}</td><th style="${thStyle}">퇴사(예정)일</th><td style="${tdStyle}">${e(d.resignDate)}</td></tr>
|
||||||
|
<tr><th style="${thStyle}">주 소</th><td style="${tdStyle}" colspan="3">${e(d.address)}</td></tr>
|
||||||
|
<tr><th style="${thStyle}">사 유</th><td style="${tdStyle}" colspan="3">${e(d.reason)}</td></tr>
|
||||||
|
</table>
|
||||||
|
<p style="text-align:center; font-size:15px; line-height:1.8; margin:36px 0;">상기 본인은 위 사유로 인하여 사직하고자<br>이에 사직서를 제출하오니 허가하여 주시기 바랍니다.</p>
|
||||||
|
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:24px;">${e(d.issueDateFormatted)}</p>
|
||||||
|
<p style="text-align:center; font-size:14px; margin-bottom:48px;">신청인 ${e(d.name)} (인)</p>
|
||||||
|
<div style="text-align:center; margin-top:32px;">
|
||||||
|
<p style="font-size:16px; font-weight:600;">${e(d.company)} 대표이사 귀하</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
@endif
|
||||||
|
|
||||||
@if(!empty($approval->content) && $approval->form?->code === 'appointment_cert')
|
@if(!empty($approval->content) && $approval->form?->code === 'appointment_cert')
|
||||||
const _appointmentCertContent = @json($approval->content);
|
const _appointmentCertContent = @json($approval->content);
|
||||||
|
|
||||||
|
|||||||
@@ -981,6 +981,10 @@
|
|||||||
Route::get('/appointment-cert-info/{userId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'appointmentCertInfo'])->name('appointment-cert-info');
|
Route::get('/appointment-cert-info/{userId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'appointmentCertInfo'])->name('appointment-cert-info');
|
||||||
Route::get('/{id}/appointment-cert-pdf', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'appointmentCertPdf'])->name('appointment-cert-pdf');
|
Route::get('/{id}/appointment-cert-pdf', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'appointmentCertPdf'])->name('appointment-cert-pdf');
|
||||||
|
|
||||||
|
// 사직서
|
||||||
|
Route::get('/resignation-info/{userId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'resignationInfo'])->name('resignation-info');
|
||||||
|
Route::get('/{id}/resignation-pdf', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'resignationPdf'])->name('resignation-pdf');
|
||||||
|
|
||||||
// CRUD
|
// CRUD
|
||||||
Route::post('/', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'store'])->name('store');
|
Route::post('/', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'store'])->name('store');
|
||||||
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'show'])->name('show');
|
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'show'])->name('show');
|
||||||
|
|||||||
Reference in New Issue
Block a user