feat: [결재] 양식 마이그레이션 12종 + 반려이력/재상신
- 재직/경력/위촉증명서, 사직서, 사용인감계, 위임장 - 이사회의사록, 견적서, 공문서, 연차사용촉진 1차/2차 - 지출결의서 body_template 고도화 - rejection_history, resubmit_count, drafter_read_at 컬럼 - Document-Approval 브릿지 연동 (linkable) - 수신함 날짜 범위 필터 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ public function rules(): array
|
|||||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||||
'per_page' => 'nullable|integer|min:1',
|
'per_page' => 'nullable|integer|min:1',
|
||||||
'page' => 'nullable|integer|min:1',
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'start_date' => 'nullable|date_format:Y-m-d',
|
||||||
|
'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +56,8 @@ class Approval extends Model
|
|||||||
'completed_at',
|
'completed_at',
|
||||||
'current_step',
|
'current_step',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'linkable_type',
|
||||||
|
'linkable_id',
|
||||||
'created_by',
|
'created_by',
|
||||||
'updated_by',
|
'updated_by',
|
||||||
'deleted_by',
|
'deleted_by',
|
||||||
@@ -135,6 +138,14 @@ public function referenceSteps(): HasMany
|
|||||||
->orderBy('step_order');
|
->orderBy('step_order');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연결 대상 (Document 등)
|
||||||
|
*/
|
||||||
|
public function linkable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 생성자
|
* 생성자
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Documents\Document;
|
||||||
use App\Models\Tenants\Approval;
|
use App\Models\Tenants\Approval;
|
||||||
use App\Models\Tenants\ApprovalForm;
|
use App\Models\Tenants\ApprovalForm;
|
||||||
use App\Models\Tenants\ApprovalLine;
|
use App\Models\Tenants\ApprovalLine;
|
||||||
@@ -446,6 +447,14 @@ public function inbox(array $params): LengthAwarePaginator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 날짜 범위 필터
|
||||||
|
if (! empty($params['start_date'])) {
|
||||||
|
$query->whereDate('created_at', '>=', $params['start_date']);
|
||||||
|
}
|
||||||
|
if (! empty($params['end_date'])) {
|
||||||
|
$query->whereDate('created_at', '<=', $params['end_date']);
|
||||||
|
}
|
||||||
|
|
||||||
// 정렬
|
// 정렬
|
||||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||||
@@ -559,7 +568,7 @@ public function show(int $id): Approval
|
|||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
return Approval::query()
|
$approval = Approval::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->with([
|
->with([
|
||||||
'form:id,name,code,category,template',
|
'form:id,name,code,category,template',
|
||||||
@@ -571,6 +580,19 @@ public function show(int $id): Approval
|
|||||||
'steps.approver.tenantProfile.department:id,name',
|
'steps.approver.tenantProfile.department:id,name',
|
||||||
])
|
])
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
|
// Document 브릿지: 연결된 문서 데이터 로딩
|
||||||
|
if ($approval->linkable_type === Document::class) {
|
||||||
|
$approval->load([
|
||||||
|
'linkable.template',
|
||||||
|
'linkable.template.approvalLines',
|
||||||
|
'linkable.data',
|
||||||
|
'linkable.approvals.user:id,name',
|
||||||
|
'linkable.attachments',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $approval;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -834,6 +856,9 @@ public function approve(int $id, ?string $comment = null): Approval
|
|||||||
$approval->updated_by = $userId;
|
$approval->updated_by = $userId;
|
||||||
$approval->save();
|
$approval->save();
|
||||||
|
|
||||||
|
// Document 브릿지 동기화
|
||||||
|
$this->syncToLinkedDocument($approval);
|
||||||
|
|
||||||
return $approval->fresh([
|
return $approval->fresh([
|
||||||
'form:id,name,code,category',
|
'form:id,name,code,category',
|
||||||
'drafter:id,name',
|
'drafter:id,name',
|
||||||
@@ -887,6 +912,9 @@ public function reject(int $id, string $comment): Approval
|
|||||||
$approval->updated_by = $userId;
|
$approval->updated_by = $userId;
|
||||||
$approval->save();
|
$approval->save();
|
||||||
|
|
||||||
|
// Document 브릿지 동기화
|
||||||
|
$this->syncToLinkedDocument($approval);
|
||||||
|
|
||||||
return $approval->fresh([
|
return $approval->fresh([
|
||||||
'form:id,name,code,category',
|
'form:id,name,code,category',
|
||||||
'drafter:id,name',
|
'drafter:id,name',
|
||||||
@@ -926,6 +954,9 @@ public function cancel(int $id): Approval
|
|||||||
$approval->updated_by = $userId;
|
$approval->updated_by = $userId;
|
||||||
$approval->save();
|
$approval->save();
|
||||||
|
|
||||||
|
// Document 브릿지 동기화 (steps 삭제 전에 실행)
|
||||||
|
$this->syncToLinkedDocument($approval);
|
||||||
|
|
||||||
// 결재 단계들 삭제
|
// 결재 단계들 삭제
|
||||||
$approval->steps()->delete();
|
$approval->steps()->delete();
|
||||||
|
|
||||||
@@ -936,6 +967,57 @@ public function cancel(int $id): Approval
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approval → Document 브릿지 동기화
|
||||||
|
* 결재 승인/반려/회수 시 연결된 Document의 상태와 결재란을 동기화
|
||||||
|
*/
|
||||||
|
private function syncToLinkedDocument(Approval $approval): void
|
||||||
|
{
|
||||||
|
if ($approval->linkable_type !== Document::class) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = Document::find($approval->linkable_id);
|
||||||
|
if (! $document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// approval_steps → document_approvals 동기화 (승인자 이름/시각 반영)
|
||||||
|
foreach ($approval->steps as $step) {
|
||||||
|
if ($step->status === ApprovalStep::STATUS_PENDING) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$docApproval = $document->approvals()
|
||||||
|
->where('step', $step->step_order)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($docApproval) {
|
||||||
|
$docApproval->update([
|
||||||
|
'status' => strtoupper($step->status),
|
||||||
|
'acted_at' => $step->acted_at,
|
||||||
|
'comment' => $step->comment,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document 전체 상태 동기화
|
||||||
|
$documentStatus = match ($approval->status) {
|
||||||
|
Approval::STATUS_APPROVED => Document::STATUS_APPROVED,
|
||||||
|
Approval::STATUS_REJECTED => Document::STATUS_REJECTED,
|
||||||
|
Approval::STATUS_CANCELLED => Document::STATUS_CANCELLED,
|
||||||
|
default => Document::STATUS_PENDING,
|
||||||
|
};
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'status' => $documentStatus,
|
||||||
|
'completed_at' => in_array($approval->status, [
|
||||||
|
Approval::STATUS_APPROVED,
|
||||||
|
Approval::STATUS_REJECTED,
|
||||||
|
]) ? now() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 참조 열람 처리
|
* 참조 열람 처리
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 결재(approvals) 테이블에 연결 대상(linkable) 컬럼 추가
|
||||||
|
* - Document 시스템과 Approval 시스템을 브릿지하기 위한 다형성 관계
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('approvals', function (Blueprint $table) {
|
||||||
|
$table->string('linkable_type')->nullable()->after('attachments')->comment('연결 대상 모델 (예: App\\Models\\Documents\\Document)');
|
||||||
|
$table->unsignedBigInteger('linkable_id')->nullable()->after('linkable_type')->comment('연결 대상 ID');
|
||||||
|
$table->index(['linkable_type', 'linkable_id'], 'idx_linkable');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('approvals', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_linkable');
|
||||||
|
$table->dropColumn(['linkable_type', 'linkable_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||||
|
|
||||||
|
foreach ($tenants as $tenantId) {
|
||||||
|
$exists = DB::table('approval_forms')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', 'resignation')
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $exists) {
|
||||||
|
DB::table('approval_forms')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'name' => '사직서',
|
||||||
|
'code' => 'resignation',
|
||||||
|
'category' => 'certificate',
|
||||||
|
'template' => '[]',
|
||||||
|
'body_template' => '',
|
||||||
|
'is_active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('approval_forms')->where('code', 'resignation')->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user