feat: [approval] 재직증명서 기안 기능 추가
- EmploymentCertService: 사원 정보 조회, DOCX 생성, 파일 레코드 생성
- API 엔드포인트: cert-info/{userId}, generate-cert-docx
- _certificate-form: 인적사항/재직사항/발급정보 입력 폼
- _certificate-show: 재직증명서 읽기전용 표시 파셜
- create/edit/show에 employment_cert 양식 분기 처리
- phpoffice/phpword 패키지 추가
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Boards\File;
|
use App\Models\Boards\File;
|
||||||
use App\Services\ApprovalService;
|
use App\Services\ApprovalService;
|
||||||
|
use App\Services\EmploymentCertService;
|
||||||
use App\Services\GoogleCloudStorageService;
|
use App\Services\GoogleCloudStorageService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -254,6 +255,74 @@ public function bulkDestroy(Request $request): JsonResponse
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 재직증명서
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사원 재직증명서 정보 조회
|
||||||
|
*/
|
||||||
|
public function certInfo(int $userId): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tenantId = session('selected_tenant_id');
|
||||||
|
$service = app(EmploymentCertService::class);
|
||||||
|
$data = $service->getCertInfo($userId, $tenantId);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'data' => $data]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '사원 정보를 불러올 수 없습니다.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재직증명서 DOCX 생성
|
||||||
|
*/
|
||||||
|
public function generateCertDocx(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'user_id' => 'required|integer',
|
||||||
|
'purpose' => 'required|string|max:200',
|
||||||
|
'address' => 'nullable|string|max:500',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tenantId = session('selected_tenant_id');
|
||||||
|
$service = app(EmploymentCertService::class);
|
||||||
|
$certInfo = $service->getCertInfo($request->input('user_id'), $tenantId);
|
||||||
|
|
||||||
|
// 주소가 수정된 경우 덮어쓰기
|
||||||
|
if ($request->filled('address')) {
|
||||||
|
$certInfo['address'] = $request->input('address');
|
||||||
|
}
|
||||||
|
$certInfo['purpose'] = $request->input('purpose');
|
||||||
|
|
||||||
|
$storagePath = $service->generateDocx($certInfo, $tenantId);
|
||||||
|
$displayName = '재직증명서_'.($certInfo['name'] ?? '').'_'.date('Ymd').'.docx';
|
||||||
|
$fileRecord = $service->createFileRecord($storagePath, $displayName, $tenantId);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'file_id' => $fileRecord->id,
|
||||||
|
'file_name' => $displayName,
|
||||||
|
],
|
||||||
|
'message' => '재직증명서가 생성되었습니다.',
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '재직증명서 생성에 실패했습니다.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 워크플로우
|
// 워크플로우
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ public function edit(Request $request, int $id): View|Response
|
|||||||
$forms = $this->service->getApprovalForms();
|
$forms = $this->service->getApprovalForms();
|
||||||
$lines = $this->service->getApprovalLines();
|
$lines = $this->service->getApprovalLines();
|
||||||
[$cards, $accounts] = $this->getCardAndAccountData();
|
[$cards, $accounts] = $this->getCardAndAccountData();
|
||||||
|
$employees = app(LeaveService::class)->getActiveEmployees();
|
||||||
|
|
||||||
return view('approvals.edit', compact('approval', 'forms', 'lines', 'cards', 'accounts'));
|
return view('approvals.edit', compact('approval', 'forms', 'lines', 'cards', 'accounts', 'employees'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
121
app/Services/EmploymentCertService.php
Normal file
121
app/Services/EmploymentCertService.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use App\Models\HR\Employee;
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use PhpOffice\PhpWord\TemplateProcessor;
|
||||||
|
|
||||||
|
class EmploymentCertService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 사원의 재직증명서 정보 조회
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
$residentNumber = $employee->resident_number;
|
||||||
|
$maskedResident = $residentNumber
|
||||||
|
? substr($residentNumber, 0, 8).'******'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $employee->user->name ?? $employee->display_name ?? '',
|
||||||
|
'resident_number' => $maskedResident,
|
||||||
|
'resident_number_full' => $residentNumber ?? '',
|
||||||
|
'address' => $employee->address ?? '',
|
||||||
|
'department' => $employee->department?->name ?? '',
|
||||||
|
'position' => $employee->position_label ?? '',
|
||||||
|
'hire_date' => $employee->hire_date ?? '',
|
||||||
|
'company_name' => $tenant->company_name ?? '',
|
||||||
|
'business_num' => $tenant->business_num ?? '',
|
||||||
|
'ceo_name' => $tenant->ceo_name ?? '',
|
||||||
|
'phone' => $tenant->phone ?? '',
|
||||||
|
'company_address' => $tenant->address ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOCX 생성
|
||||||
|
*/
|
||||||
|
public function generateDocx(array $data, int $tenantId): string
|
||||||
|
{
|
||||||
|
$templatePath = storage_path('app/templates/employment_cert.docx');
|
||||||
|
|
||||||
|
if (! file_exists($templatePath)) {
|
||||||
|
throw new \RuntimeException('재직증명서 템플릿 파일이 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$template = new TemplateProcessor($templatePath);
|
||||||
|
|
||||||
|
$hireDateFormatted = '';
|
||||||
|
if (! empty($data['hire_date'])) {
|
||||||
|
try {
|
||||||
|
$hireDateFormatted = date('Y년 m월 d일', strtotime($data['hire_date']));
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$hireDateFormatted = $data['hire_date'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$issueDateFormatted = date('Y년 m월 d일');
|
||||||
|
|
||||||
|
$template->setValue('name', $data['name'] ?? '');
|
||||||
|
$template->setValue('resident_number', $data['resident_number_full'] ?? '');
|
||||||
|
$template->setValue('address', $data['address'] ?? '');
|
||||||
|
$template->setValue('company_name', $data['company_name'] ?? '');
|
||||||
|
$template->setValue('business_num', $data['business_num'] ?? '');
|
||||||
|
$template->setValue('ceo_name', $data['ceo_name'] ?? '');
|
||||||
|
$template->setValue('phone', $data['phone'] ?? '');
|
||||||
|
$template->setValue('company_address', $data['company_address'] ?? '');
|
||||||
|
$template->setValue('department', $data['department'] ?? '');
|
||||||
|
$template->setValue('position', $data['position'] ?? '');
|
||||||
|
$template->setValue('hire_date', $hireDateFormatted);
|
||||||
|
$template->setValue('purpose', $data['purpose'] ?? '');
|
||||||
|
$template->setValue('issue_date', $issueDateFormatted);
|
||||||
|
|
||||||
|
$outputDir = storage_path("app/approvals/{$tenantId}");
|
||||||
|
if (! is_dir($outputDir)) {
|
||||||
|
mkdir($outputDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = '재직증명서_'.($data['name'] ?? 'unknown').'_'.date('Ymd').'.docx';
|
||||||
|
$storedName = Str::random(40).'.docx';
|
||||||
|
$storagePath = "approvals/{$tenantId}/{$storedName}";
|
||||||
|
$fullPath = storage_path("app/{$storagePath}");
|
||||||
|
|
||||||
|
$template->saveAs($fullPath);
|
||||||
|
|
||||||
|
return $storagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 레코드 생성 및 approval에 첨부
|
||||||
|
*/
|
||||||
|
public function createFileRecord(string $storagePath, string $displayName, int $tenantId): File
|
||||||
|
{
|
||||||
|
$fullPath = storage_path("app/{$storagePath}");
|
||||||
|
|
||||||
|
return File::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'document_type' => 'approval_attachment',
|
||||||
|
'display_name' => $displayName,
|
||||||
|
'original_name' => $displayName,
|
||||||
|
'stored_name' => basename($storagePath),
|
||||||
|
'file_path' => $storagePath,
|
||||||
|
'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'file_size' => filesize($fullPath),
|
||||||
|
'file_type' => 'docx',
|
||||||
|
'is_temp' => false,
|
||||||
|
'uploaded_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"laravel/sanctum": "^4.2",
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"phpoffice/phpspreadsheet": "^5.4",
|
"phpoffice/phpspreadsheet": "^5.4",
|
||||||
|
"phpoffice/phpword": "^1.4",
|
||||||
"setasign/fpdi": "^2.6",
|
"setasign/fpdi": "^2.6",
|
||||||
"spatie/laravel-permission": "^6.23",
|
"spatie/laravel-permission": "^6.23",
|
||||||
"stevebauman/purify": "^6.3",
|
"stevebauman/purify": "^6.3",
|
||||||
|
|||||||
162
composer.lock
generated
162
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "fa477884dd95c46333bc82ce0a64e4fb",
|
"content-hash": "36c1111268937924cbb3cad512a30a41",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -3197,6 +3197,58 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-11-20T02:34:59+00:00"
|
"time": "2025-11-20T02:34:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpoffice/math",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPOffice/Math.git",
|
||||||
|
"reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
|
||||||
|
"reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"php": "^7.1|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^9.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpOffice\\Math\\": "src/Math/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Progi1984",
|
||||||
|
"homepage": "https://lefevre.dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Math - Manipulate Math Formula",
|
||||||
|
"homepage": "https://phpoffice.github.io/Math/",
|
||||||
|
"keywords": [
|
||||||
|
"MathML",
|
||||||
|
"officemathml",
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPOffice/Math/issues",
|
||||||
|
"source": "https://github.com/PHPOffice/Math/tree/0.3.0"
|
||||||
|
},
|
||||||
|
"time": "2025-05-29T08:31:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoffice/phpspreadsheet",
|
"name": "phpoffice/phpspreadsheet",
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
@@ -3306,6 +3358,114 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-01-11T04:52:00+00:00"
|
"time": "2026-01-11T04:52:00+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpoffice/phpword",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPOffice/PHPWord.git",
|
||||||
|
"reference": "6d75328229bc93790b37e93741adf70646cea958"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958",
|
||||||
|
"reference": "6d75328229bc93790b37e93741adf70646cea958",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"php": "^7.1|^8.0",
|
||||||
|
"phpoffice/math": "^0.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.3",
|
||||||
|
"mpdf/mpdf": "^7.0 || ^8.0",
|
||||||
|
"phpmd/phpmd": "^2.13",
|
||||||
|
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||||
|
"phpunit/phpunit": ">=7.0",
|
||||||
|
"symfony/process": "^4.4 || ^5.0",
|
||||||
|
"tecnickcom/tcpdf": "^6.5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"dompdf/dompdf": "Allows writing PDF",
|
||||||
|
"ext-xmlwriter": "Allows writing OOXML and ODF",
|
||||||
|
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpOffice\\PhpWord\\": "src/PhpWord"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gabriel Bull",
|
||||||
|
"email": "me@gabrielbull.com",
|
||||||
|
"homepage": "http://gabrielbull.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Franck Lefevre",
|
||||||
|
"homepage": "https://rootslabs.net/blog/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ivan Lanin",
|
||||||
|
"homepage": "http://ivan.lanin.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roman Syroeshko",
|
||||||
|
"homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Antoine de Troostembergh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
|
||||||
|
"homepage": "https://phpoffice.github.io/PHPWord/",
|
||||||
|
"keywords": [
|
||||||
|
"ISO IEC 29500",
|
||||||
|
"OOXML",
|
||||||
|
"Office Open XML",
|
||||||
|
"OpenDocument",
|
||||||
|
"OpenXML",
|
||||||
|
"PhpOffice",
|
||||||
|
"PhpWord",
|
||||||
|
"Rich Text Format",
|
||||||
|
"WordprocessingML",
|
||||||
|
"doc",
|
||||||
|
"docx",
|
||||||
|
"html",
|
||||||
|
"odf",
|
||||||
|
"odt",
|
||||||
|
"office",
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"reader",
|
||||||
|
"rtf",
|
||||||
|
"template",
|
||||||
|
"template processor",
|
||||||
|
"word",
|
||||||
|
"writer"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPOffice/PHPWord/issues",
|
||||||
|
"source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0"
|
||||||
|
},
|
||||||
|
"time": "2025-06-05T10:32:36+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
|
|||||||
@@ -95,6 +95,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._certificate-form', [
|
||||||
|
'employees' => $employees ?? collect(),
|
||||||
|
])
|
||||||
|
|
||||||
{{-- 지출결의서 전용 폼 --}}
|
{{-- 지출결의서 전용 폼 --}}
|
||||||
@include('approvals.partials._expense-form', [
|
@include('approvals.partials._expense-form', [
|
||||||
'initialData' => [],
|
'initialData' => [],
|
||||||
@@ -217,6 +222,8 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
|
|||||||
const linesData = @json($lines);
|
const linesData = @json($lines);
|
||||||
let isExpenseForm = false;
|
let isExpenseForm = false;
|
||||||
let isLeaveForm = false;
|
let isLeaveForm = false;
|
||||||
|
let isCertForm = false;
|
||||||
|
let certFileId = null;
|
||||||
|
|
||||||
// 양식코드별 표시할 유형 목록
|
// 양식코드별 표시할 유형 목록
|
||||||
const leaveTypesByFormCode = {
|
const leaveTypesByFormCode = {
|
||||||
@@ -459,39 +466,44 @@ function switchFormMode(formId) {
|
|||||||
const code = formCodes[formId];
|
const code = formCodes[formId];
|
||||||
const expenseContainer = document.getElementById('expense-form-container');
|
const expenseContainer = document.getElementById('expense-form-container');
|
||||||
const leaveContainer = document.getElementById('leave-form-container');
|
const leaveContainer = document.getElementById('leave-form-container');
|
||||||
|
const certContainer = document.getElementById('cert-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');
|
||||||
|
|
||||||
const leaveFormCodes = ['leave', 'attendance_request', 'reason_report'];
|
const leaveFormCodes = ['leave', 'attendance_request', 'reason_report'];
|
||||||
|
|
||||||
if (code === 'expense') {
|
// 모두 숨기기
|
||||||
isExpenseForm = true;
|
|
||||||
isLeaveForm = false;
|
|
||||||
expenseContainer.style.display = '';
|
|
||||||
leaveContainer.style.display = 'none';
|
|
||||||
expenseLoadBtn.style.display = '';
|
|
||||||
bodyArea.style.display = 'none';
|
|
||||||
} else if (leaveFormCodes.includes(code)) {
|
|
||||||
isExpenseForm = false;
|
|
||||||
isLeaveForm = true;
|
|
||||||
expenseContainer.style.display = 'none';
|
expenseContainer.style.display = 'none';
|
||||||
leaveContainer.style.display = '';
|
leaveContainer.style.display = 'none';
|
||||||
|
certContainer.style.display = 'none';
|
||||||
expenseLoadBtn.style.display = 'none';
|
expenseLoadBtn.style.display = 'none';
|
||||||
bodyArea.style.display = 'none';
|
bodyArea.style.display = 'none';
|
||||||
|
isExpenseForm = false;
|
||||||
|
isLeaveForm = false;
|
||||||
|
isCertForm = false;
|
||||||
|
|
||||||
|
if (code === 'expense') {
|
||||||
|
isExpenseForm = true;
|
||||||
|
expenseContainer.style.display = '';
|
||||||
|
expenseLoadBtn.style.display = '';
|
||||||
|
} else if (leaveFormCodes.includes(code)) {
|
||||||
|
isLeaveForm = true;
|
||||||
|
leaveContainer.style.display = '';
|
||||||
populateLeaveTypes(code);
|
populateLeaveTypes(code);
|
||||||
|
|
||||||
// 시작일/종료일 기본값: 오늘
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const startEl = document.getElementById('leave-start-date');
|
const startEl = document.getElementById('leave-start-date');
|
||||||
const endEl = document.getElementById('leave-end-date');
|
const endEl = document.getElementById('leave-end-date');
|
||||||
if (!startEl.value) startEl.value = today;
|
if (!startEl.value) startEl.value = today;
|
||||||
if (!endEl.value) endEl.value = today;
|
if (!endEl.value) endEl.value = today;
|
||||||
|
} else if (code === 'employment_cert') {
|
||||||
|
isCertForm = true;
|
||||||
|
certContainer.style.display = '';
|
||||||
|
certFileId = null;
|
||||||
|
// 초기 사원 정보 로드
|
||||||
|
const certUserId = document.getElementById('cert-user-id').value;
|
||||||
|
if (certUserId) loadCertInfo(certUserId);
|
||||||
} else {
|
} else {
|
||||||
isExpenseForm = false;
|
|
||||||
isLeaveForm = false;
|
|
||||||
expenseContainer.style.display = 'none';
|
|
||||||
leaveContainer.style.display = 'none';
|
|
||||||
expenseLoadBtn.style.display = 'none';
|
|
||||||
bodyArea.style.display = '';
|
bodyArea.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,7 +513,7 @@ function applyBodyTemplate(formId) {
|
|||||||
switchFormMode(formId);
|
switchFormMode(formId);
|
||||||
|
|
||||||
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
||||||
if (isExpenseForm || isLeaveForm) {
|
if (isExpenseForm || isLeaveForm || isCertForm) {
|
||||||
const titleEl = document.getElementById('title');
|
const titleEl = document.getElementById('title');
|
||||||
if (!titleEl.value.trim()) {
|
if (!titleEl.value.trim()) {
|
||||||
const formSelect = document.getElementById('form_id');
|
const formSelect = document.getElementById('form_id');
|
||||||
@@ -608,6 +620,56 @@ function applyBodyTemplate(formId) {
|
|||||||
|
|
||||||
formContent = leaveData;
|
formContent = leaveData;
|
||||||
formBody = buildLeaveBody(leaveData);
|
formBody = buildLeaveBody(leaveData);
|
||||||
|
} else if (isCertForm) {
|
||||||
|
const purpose = getCertPurpose();
|
||||||
|
if (!purpose) {
|
||||||
|
showToast('사용용도를 입력해주세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formContent = {
|
||||||
|
cert_user_id: document.getElementById('cert-user-id').value,
|
||||||
|
name: document.getElementById('cert-name').value,
|
||||||
|
resident_number: document.getElementById('cert-resident').value,
|
||||||
|
address: document.getElementById('cert-address').value,
|
||||||
|
department: document.getElementById('cert-department').value,
|
||||||
|
position: document.getElementById('cert-position').value,
|
||||||
|
hire_date: document.getElementById('cert-hire-date').value,
|
||||||
|
company_name: document.getElementById('cert-company').value,
|
||||||
|
business_num: document.getElementById('cert-business-num').value,
|
||||||
|
purpose: purpose,
|
||||||
|
issue_date: document.getElementById('cert-issue-date').value,
|
||||||
|
};
|
||||||
|
formBody = null;
|
||||||
|
|
||||||
|
// DOCX 생성
|
||||||
|
if (!certFileId) {
|
||||||
|
showToast('재직증명서 DOCX를 생성 중입니다...', 'info');
|
||||||
|
try {
|
||||||
|
const certResp = await fetch('/api/admin/approvals/generate-cert-docx', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: formContent.cert_user_id,
|
||||||
|
purpose: purpose,
|
||||||
|
address: formContent.address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const certData = await certResp.json();
|
||||||
|
if (certData.success) {
|
||||||
|
certFileId = certData.data.file_id;
|
||||||
|
} else {
|
||||||
|
showToast(certData.message || 'DOCX 생성 실패', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('DOCX 생성 중 오류가 발생했습니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (certFileId) {
|
||||||
|
attachmentFileIds.push(certFileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -787,6 +849,58 @@ function closeExpenseLoadModal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 재직증명서 관련 함수
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
async function loadCertInfo(userId) {
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/admin/approvals/cert-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('cert-name').value = d.name || '';
|
||||||
|
document.getElementById('cert-resident').value = d.resident_number || '';
|
||||||
|
document.getElementById('cert-address').value = d.address || '';
|
||||||
|
document.getElementById('cert-company').value = d.company_name || '';
|
||||||
|
document.getElementById('cert-business-num').value = d.business_num || '';
|
||||||
|
document.getElementById('cert-department').value = d.department || '';
|
||||||
|
document.getElementById('cert-position').value = d.position || '';
|
||||||
|
document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : '';
|
||||||
|
certFileId = null; // 사원 변경 시 DOCX 재생성 필요
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCertPurposeChange() {
|
||||||
|
const sel = document.getElementById('cert-purpose-select');
|
||||||
|
const customWrap = document.getElementById('cert-purpose-custom-wrap');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
customWrap.style.display = '';
|
||||||
|
document.getElementById('cert-purpose-custom').focus();
|
||||||
|
} else {
|
||||||
|
customWrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
certFileId = null; // 용도 변경 시 DOCX 재생성 필요
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertPurpose() {
|
||||||
|
const sel = document.getElementById('cert-purpose-select');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
return document.getElementById('cert-purpose-custom').value.trim();
|
||||||
|
}
|
||||||
|
return sel.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
|||||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 재직증명서 전용 폼 --}}
|
||||||
|
@include('approvals.partials._certificate-form', [
|
||||||
|
'employees' => $employees ?? collect(),
|
||||||
|
])
|
||||||
|
|
||||||
{{-- 지출결의서 전용 폼 --}}
|
{{-- 지출결의서 전용 폼 --}}
|
||||||
@php
|
@php
|
||||||
$existingFiles = [];
|
$existingFiles = [];
|
||||||
@@ -254,6 +259,8 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
|||||||
const formCodes = @json($forms->pluck('code', 'id'));
|
const formCodes = @json($forms->pluck('code', 'id'));
|
||||||
const linesData = @json($lines);
|
const linesData = @json($lines);
|
||||||
let isExpenseForm = false;
|
let isExpenseForm = false;
|
||||||
|
let isCertForm = false;
|
||||||
|
let certFileId = null;
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
@@ -418,15 +425,25 @@ function applyQuickLine(lineId) {
|
|||||||
function switchFormMode(formId) {
|
function switchFormMode(formId) {
|
||||||
const code = formCodes[formId];
|
const code = formCodes[formId];
|
||||||
const expenseContainer = document.getElementById('expense-form-container');
|
const expenseContainer = document.getElementById('expense-form-container');
|
||||||
|
const certContainer = document.getElementById('cert-form-container');
|
||||||
const bodyArea = document.getElementById('body-area');
|
const bodyArea = document.getElementById('body-area');
|
||||||
|
|
||||||
|
expenseContainer.style.display = 'none';
|
||||||
|
certContainer.style.display = 'none';
|
||||||
|
bodyArea.style.display = 'none';
|
||||||
|
isExpenseForm = false;
|
||||||
|
isCertForm = false;
|
||||||
|
|
||||||
if (code === 'expense') {
|
if (code === 'expense') {
|
||||||
isExpenseForm = true;
|
isExpenseForm = true;
|
||||||
expenseContainer.style.display = '';
|
expenseContainer.style.display = '';
|
||||||
bodyArea.style.display = 'none';
|
} else if (code === 'employment_cert') {
|
||||||
|
isCertForm = true;
|
||||||
|
certContainer.style.display = '';
|
||||||
|
certFileId = null;
|
||||||
|
const certUserId = document.getElementById('cert-user-id').value;
|
||||||
|
if (certUserId) loadCertInfo(certUserId);
|
||||||
} else {
|
} else {
|
||||||
isExpenseForm = false;
|
|
||||||
expenseContainer.style.display = 'none';
|
|
||||||
bodyArea.style.display = '';
|
bodyArea.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +453,7 @@ function applyBodyTemplate(formId) {
|
|||||||
switchFormMode(formId);
|
switchFormMode(formId);
|
||||||
|
|
||||||
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
||||||
if (isExpenseForm) {
|
if (isExpenseForm || isCertForm) {
|
||||||
const titleEl = document.getElementById('title');
|
const titleEl = document.getElementById('title');
|
||||||
if (!titleEl.value.trim()) {
|
if (!titleEl.value.trim()) {
|
||||||
const formSelect = document.getElementById('form_id');
|
const formSelect = document.getElementById('form_id');
|
||||||
@@ -487,8 +504,41 @@ function applyBodyTemplate(formId) {
|
|||||||
// 초기 양식에 대한 폼 모드 전환
|
// 초기 양식에 대한 폼 모드 전환
|
||||||
switchFormMode(document.getElementById('form_id').value);
|
switchFormMode(document.getElementById('form_id').value);
|
||||||
|
|
||||||
|
// 재직증명서 기존 데이터 복원
|
||||||
|
if (isCertForm) {
|
||||||
|
const certContent = @json($approval->content ?? []);
|
||||||
|
if (certContent.name) {
|
||||||
|
document.getElementById('cert-name').value = certContent.name || '';
|
||||||
|
document.getElementById('cert-resident').value = certContent.resident_number || '';
|
||||||
|
document.getElementById('cert-address').value = certContent.address || '';
|
||||||
|
document.getElementById('cert-company').value = certContent.company_name || '';
|
||||||
|
document.getElementById('cert-business-num').value = certContent.business_num || '';
|
||||||
|
document.getElementById('cert-department').value = certContent.department || '';
|
||||||
|
document.getElementById('cert-position').value = certContent.position || '';
|
||||||
|
document.getElementById('cert-hire-date').value = certContent.hire_date || '';
|
||||||
|
document.getElementById('cert-issue-date').value = certContent.issue_date || '{{ now()->format("Y-m-d") }}';
|
||||||
|
|
||||||
|
// 용도 복원
|
||||||
|
const purposeSelect = document.getElementById('cert-purpose-select');
|
||||||
|
const purpose = certContent.purpose || '';
|
||||||
|
const predefined = ['은행 제출용', '관공서 제출용', '비자 신청용', '대출 신청용'];
|
||||||
|
if (predefined.includes(purpose)) {
|
||||||
|
purposeSelect.value = purpose;
|
||||||
|
} else if (purpose) {
|
||||||
|
purposeSelect.value = '__custom__';
|
||||||
|
document.getElementById('cert-purpose-custom-wrap').style.display = '';
|
||||||
|
document.getElementById('cert-purpose-custom').value = purpose;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사원 셀렉트 복원
|
||||||
|
if (certContent.cert_user_id) {
|
||||||
|
document.getElementById('cert-user-id').value = certContent.cert_user_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
|
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
|
||||||
if (!isExpenseForm) {
|
if (!isExpenseForm && !isCertForm) {
|
||||||
const existingBody = document.getElementById('body').value;
|
const existingBody = document.getElementById('body').value;
|
||||||
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
|
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
|
||||||
document.getElementById('useEditor').checked = true;
|
document.getElementById('useEditor').checked = true;
|
||||||
@@ -520,21 +570,74 @@ function applyBodyTemplate(formId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let expenseContent = {};
|
let formContent = {};
|
||||||
|
let formBody = getBodyContent();
|
||||||
let attachmentFileIds = [];
|
let attachmentFileIds = [];
|
||||||
|
|
||||||
if (isExpenseForm) {
|
if (isExpenseForm) {
|
||||||
const expenseEl = document.getElementById('expense-form-container');
|
const expenseEl = document.getElementById('expense-form-container');
|
||||||
if (expenseEl && expenseEl._x_dataStack) {
|
if (expenseEl && expenseEl._x_dataStack) {
|
||||||
expenseContent = expenseEl._x_dataStack[0].getFormData();
|
formContent = expenseEl._x_dataStack[0].getFormData();
|
||||||
attachmentFileIds = expenseEl._x_dataStack[0].getFileIds();
|
attachmentFileIds = expenseEl._x_dataStack[0].getFileIds();
|
||||||
}
|
}
|
||||||
|
formBody = null;
|
||||||
|
} else if (isCertForm) {
|
||||||
|
const purpose = getCertPurpose();
|
||||||
|
if (!purpose) {
|
||||||
|
showToast('사용용도를 입력해주세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formContent = {
|
||||||
|
cert_user_id: document.getElementById('cert-user-id').value,
|
||||||
|
name: document.getElementById('cert-name').value,
|
||||||
|
resident_number: document.getElementById('cert-resident').value,
|
||||||
|
address: document.getElementById('cert-address').value,
|
||||||
|
department: document.getElementById('cert-department').value,
|
||||||
|
position: document.getElementById('cert-position').value,
|
||||||
|
hire_date: document.getElementById('cert-hire-date').value,
|
||||||
|
company_name: document.getElementById('cert-company').value,
|
||||||
|
business_num: document.getElementById('cert-business-num').value,
|
||||||
|
purpose: purpose,
|
||||||
|
issue_date: document.getElementById('cert-issue-date').value,
|
||||||
|
};
|
||||||
|
formBody = null;
|
||||||
|
|
||||||
|
// DOCX 생성
|
||||||
|
if (!certFileId) {
|
||||||
|
showToast('재직증명서 DOCX를 생성 중입니다...', 'info');
|
||||||
|
try {
|
||||||
|
const certResp = await fetch('/api/admin/approvals/generate-cert-docx', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: formContent.cert_user_id,
|
||||||
|
purpose: purpose,
|
||||||
|
address: formContent.address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const certData = await certResp.json();
|
||||||
|
if (certData.success) {
|
||||||
|
certFileId = certData.data.file_id;
|
||||||
|
} else {
|
||||||
|
showToast(certData.message || 'DOCX 생성 실패', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('DOCX 생성 중 오류가 발생했습니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (certFileId) {
|
||||||
|
attachmentFileIds.push(certFileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
form_id: document.getElementById('form_id').value,
|
form_id: document.getElementById('form_id').value,
|
||||||
title: title,
|
title: title,
|
||||||
body: isExpenseForm ? null : getBodyContent(),
|
body: formBody,
|
||||||
content: isExpenseForm ? expenseContent : {},
|
content: formContent,
|
||||||
attachment_file_ids: attachmentFileIds,
|
attachment_file_ids: attachmentFileIds,
|
||||||
is_urgent: document.getElementById('is_urgent').checked,
|
is_urgent: document.getElementById('is_urgent').checked,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
@@ -608,5 +711,57 @@ function applyBodyTemplate(formId) {
|
|||||||
showToast('서버 오류가 발생했습니다.', 'error');
|
showToast('서버 오류가 발생했습니다.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 재직증명서 관련 함수
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
async function loadCertInfo(userId) {
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/admin/approvals/cert-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('cert-name').value = d.name || '';
|
||||||
|
document.getElementById('cert-resident').value = d.resident_number || '';
|
||||||
|
document.getElementById('cert-address').value = d.address || '';
|
||||||
|
document.getElementById('cert-company').value = d.company_name || '';
|
||||||
|
document.getElementById('cert-business-num').value = d.business_num || '';
|
||||||
|
document.getElementById('cert-department').value = d.department || '';
|
||||||
|
document.getElementById('cert-position').value = d.position || '';
|
||||||
|
document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : '';
|
||||||
|
certFileId = null;
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCertPurposeChange() {
|
||||||
|
const sel = document.getElementById('cert-purpose-select');
|
||||||
|
const customWrap = document.getElementById('cert-purpose-custom-wrap');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
customWrap.style.display = '';
|
||||||
|
document.getElementById('cert-purpose-custom').focus();
|
||||||
|
} else {
|
||||||
|
customWrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
certFileId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertPurpose() {
|
||||||
|
const sel = document.getElementById('cert-purpose-select');
|
||||||
|
if (sel.value === '__custom__') {
|
||||||
|
return document.getElementById('cert-purpose-custom').value.trim();
|
||||||
|
}
|
||||||
|
return sel.value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
124
resources/views/approvals/partials/_certificate-form.blade.php
Normal file
124
resources/views/approvals/partials/_certificate-form.blade.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{{--
|
||||||
|
재직증명서 전용 폼
|
||||||
|
Props:
|
||||||
|
$employees (Collection) - 활성 사원 목록
|
||||||
|
--}}
|
||||||
|
@php
|
||||||
|
$employees = $employees ?? collect();
|
||||||
|
$currentUserId = auth()->id();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div id="cert-form-container" style="display: none;" class="mb-4">
|
||||||
|
<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>
|
||||||
|
<select id="cert-user-id" onchange="loadCertInfo(this.value)"
|
||||||
|
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"
|
||||||
|
style="max-width: 300px;">
|
||||||
|
@foreach($employees as $emp)
|
||||||
|
<option value="{{ $emp->user_id }}" {{ $emp->user_id == $currentUserId ? 'selected' : '' }}>
|
||||||
|
{{ $emp->display_name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</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">1. 인적사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">성명</label>
|
||||||
|
<input type="text" id="cert-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: 300px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">주민등록번호</label>
|
||||||
|
<input type="text" id="cert-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>
|
||||||
|
<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="cert-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">2. 재직사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<div style="flex: 1 1 250px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">회사명</label>
|
||||||
|
<input type="text" id="cert-company" 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="cert-business-num" 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="cert-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: 200px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">직급</label>
|
||||||
|
<input type="text" id="cert-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 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="cert-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>
|
||||||
|
</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">3. 발급정보</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<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="cert-purpose-select" onchange="onCertPurposeChange()"
|
||||||
|
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="__custom__">기타 (직접입력)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="cert-purpose-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="cert-purpose-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 style="max-width: 200px;">
|
||||||
|
<label class="block text-xs font-medium text-gray-500 mb-1">발급일</label>
|
||||||
|
<input type="text" id="cert-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>
|
||||||
|
</div>
|
||||||
101
resources/views/approvals/partials/_certificate-show.blade.php
Normal file
101
resources/views/approvals/partials/_certificate-show.blade.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{{--
|
||||||
|
재직증명서 읽기전용 렌더링
|
||||||
|
Props:
|
||||||
|
$content (array) - approvals.content JSON
|
||||||
|
--}}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{{-- 인적사항 --}}
|
||||||
|
<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">1. 인적사항</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['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 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">2. 재직사항</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['company_name'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">사업자번호</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['business_num'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<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['hire_date'] ?? '-' }}</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">3. 발급정보</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['purpose'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">발급일</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['issue_date'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 첨부파일 --}}
|
||||||
|
@if(!empty($approval->attachments))
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">첨부파일</span>
|
||||||
|
<div class="mt-1 space-y-1">
|
||||||
|
@foreach($approval->attachments as $file)
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/>
|
||||||
|
</svg>
|
||||||
|
<a href="{{ route('api.admin.approvals.download-file', $file['id']) }}" class="text-blue-600 hover:underline" target="_blank">
|
||||||
|
{{ $file['name'] ?? '파일' }}
|
||||||
|
</a>
|
||||||
|
<span class="text-xs text-gray-400">
|
||||||
|
{{ isset($file['size']) ? number_format($file['size'] / 1024, 1) . 'KB' : '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -82,6 +82,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
|
|||||||
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ $approval->title }}</h2>
|
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ $approval->title }}</h2>
|
||||||
@if(!empty($approval->content) && $approval->form?->code === 'expense')
|
@if(!empty($approval->content) && $approval->form?->code === 'expense')
|
||||||
@include('approvals.partials._expense-show', ['content' => $approval->content])
|
@include('approvals.partials._expense-show', ['content' => $approval->content])
|
||||||
|
@elseif(!empty($approval->content) && $approval->form?->code === 'employment_cert')
|
||||||
|
@include('approvals.partials._certificate-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>') !!}
|
||||||
|
|||||||
@@ -969,6 +969,10 @@
|
|||||||
// 선택삭제
|
// 선택삭제
|
||||||
Route::post('/bulk-delete', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'bulkDestroy'])->name('bulk-destroy');
|
Route::post('/bulk-delete', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'bulkDestroy'])->name('bulk-destroy');
|
||||||
|
|
||||||
|
// 재직증명서
|
||||||
|
Route::get('/cert-info/{userId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'certInfo'])->name('cert-info');
|
||||||
|
Route::post('/generate-cert-docx', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'generateCertDocx'])->name('generate-cert-docx');
|
||||||
|
|
||||||
// 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