feat: [material] 부적합관리 결재 연동 구현
- Migration: approval_id FK 추가
- Model: approval() BelongsTo 관계
- Service: submitForApproval() 결재상신 (결재문서+결재선 생성)
- ApprovalService: 승인→CLOSED, 반려/회수→approval_id 해제
- Controller: POST /{id}/submit-approval 엔드포인트
- Route: submit-approval 라우트 등록
This commit is contained in:
@@ -66,4 +66,19 @@ public function changeStatus(Request $request, int $id): JsonResponse
|
||||
return $this->service->changeStatus($id, $request->input('status'));
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function submitApproval(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'nullable|string|max:200',
|
||||
'form_id' => 'nullable|integer',
|
||||
'steps' => 'required|array|min:1',
|
||||
'steps.*.approver_id' => 'required|integer',
|
||||
'steps.*.step_type' => 'nullable|string|in:approval,agreement,reference',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->submitForApproval($id, $request->all());
|
||||
}, __('message.created'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use App\Models\Departments\Department;
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Tenants\Approval;
|
||||
use App\Models\Users\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
@@ -24,6 +25,7 @@ class NonconformingReport extends Model
|
||||
'tenant_id',
|
||||
'nc_number',
|
||||
'status',
|
||||
'approval_id',
|
||||
'nc_type',
|
||||
'occurred_at',
|
||||
'confirmed_at',
|
||||
@@ -104,6 +106,11 @@ public function items(): HasMany
|
||||
return $this->hasMany(NonconformingReportItem::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function approval(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Approval::class);
|
||||
}
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use App\Models\Materials\NonconformingReport;
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Tenants\Approval;
|
||||
use App\Models\Tenants\ApprovalDelegation;
|
||||
@@ -903,6 +904,7 @@ public function approve(int $id, ?string $comment = null): Approval
|
||||
// Leave 연동 (승인 완료 시)
|
||||
if ($approval->status === Approval::STATUS_APPROVED) {
|
||||
$this->handleApprovalCompleted($approval);
|
||||
$this->handleNonconformingApproved($approval);
|
||||
}
|
||||
|
||||
return $approval->fresh([
|
||||
@@ -964,6 +966,7 @@ public function reject(int $id, string $comment): Approval
|
||||
|
||||
// Leave 연동 (반려 시)
|
||||
$this->handleApprovalRejected($approval, $comment);
|
||||
$this->handleNonconformingRejected($approval);
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
@@ -1027,6 +1030,7 @@ public function cancel(int $id, ?string $recallReason = null): Approval
|
||||
|
||||
// Leave 연동 (회수 시)
|
||||
$this->handleApprovalCancelled($approval);
|
||||
$this->handleNonconformingCancelled($approval);
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
@@ -1172,6 +1176,7 @@ public function preDecide(int $id, ?string $comment = null): Approval
|
||||
|
||||
// Leave 연동 (승인 완료)
|
||||
$this->handleApprovalCompleted($approval);
|
||||
$this->handleNonconformingApproved($approval);
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
@@ -1732,6 +1737,64 @@ private function createLeaveFromApproval(Approval $approval): Leave
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 부적합관리 연동
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 부적합 결재 승인 시 → CLOSED
|
||||
*/
|
||||
private function handleNonconformingApproved(Approval $approval): void
|
||||
{
|
||||
if ($approval->linkable_type !== NonconformingReport::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$report = NonconformingReport::find($approval->linkable_id);
|
||||
if ($report && $report->status === NonconformingReport::STATUS_RESOLVED) {
|
||||
$report->update([
|
||||
'status' => NonconformingReport::STATUS_CLOSED,
|
||||
'updated_by' => $approval->updated_by,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 결재 반려 시 → RESOLVED로 유지 (재상신 가능)
|
||||
*/
|
||||
private function handleNonconformingRejected(Approval $approval): void
|
||||
{
|
||||
if ($approval->linkable_type !== NonconformingReport::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$report = NonconformingReport::find($approval->linkable_id);
|
||||
if ($report) {
|
||||
$report->update([
|
||||
'approval_id' => null,
|
||||
'updated_by' => $approval->updated_by,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 결재 회수 시 → 결재 연결 해제
|
||||
*/
|
||||
private function handleNonconformingCancelled(Approval $approval): void
|
||||
{
|
||||
if ($approval->linkable_type !== NonconformingReport::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$report = NonconformingReport::find($approval->linkable_id);
|
||||
if ($report) {
|
||||
$report->update([
|
||||
'approval_id' => null,
|
||||
'updated_by' => $approval->updated_by,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 위임 관리
|
||||
// =========================================================================
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
use App\Models\Materials\NonconformingReport;
|
||||
use App\Models\Materials\NonconformingReportItem;
|
||||
use App\Models\Tenants\Approval;
|
||||
use App\Models\Tenants\ApprovalLine;
|
||||
use App\Models\Tenants\ApprovalStep;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class NonconformingReportService extends Service
|
||||
@@ -68,6 +71,9 @@ public function show(int $id): NonconformingReport
|
||||
'actionManager:id,name',
|
||||
'relatedEmployee:id,name',
|
||||
'files',
|
||||
'approval:id,document_number,status,completed_at',
|
||||
'approval.steps:id,approval_id,step_order,step_type,approver_id,status,comment,acted_at',
|
||||
'approval.steps.approver:id,name',
|
||||
])
|
||||
->findOrFail($id);
|
||||
}
|
||||
@@ -235,6 +241,80 @@ public function stats(array $params): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재상신 (RESOLVED 상태에서만 가능)
|
||||
*/
|
||||
public function submitForApproval(int $id, array $data): NonconformingReport
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$report = NonconformingReport::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if ($report->status !== NonconformingReport::STATUS_RESOLVED) {
|
||||
abort(422, __('error.nonconforming.must_be_resolved_for_approval'));
|
||||
}
|
||||
|
||||
if ($report->approval_id) {
|
||||
abort(422, __('error.nonconforming.approval_already_exists'));
|
||||
}
|
||||
|
||||
$steps = $data['steps'] ?? [];
|
||||
if (empty($steps)) {
|
||||
abort(422, __('error.approval.steps_required'));
|
||||
}
|
||||
|
||||
// 결재 문서 생성
|
||||
$approval = Approval::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => $this->generateApprovalNumber($tenantId),
|
||||
'form_id' => $data['form_id'] ?? null,
|
||||
'title' => $data['title'] ?? "부적합 처리 결재 - {$report->nc_number}",
|
||||
'content' => [
|
||||
'nc_number' => $report->nc_number,
|
||||
'nc_type' => $report->nc_type,
|
||||
'site_name' => $report->site_name,
|
||||
'defect_description' => $report->defect_description,
|
||||
'cause_analysis' => $report->cause_analysis,
|
||||
'corrective_action' => $report->corrective_action,
|
||||
'total_cost' => $report->total_cost,
|
||||
],
|
||||
'status' => Approval::STATUS_PENDING,
|
||||
'drafter_id' => $userId,
|
||||
'department_id' => $report->department_id,
|
||||
'drafted_at' => now(),
|
||||
'current_step' => 1,
|
||||
'linkable_type' => NonconformingReport::class,
|
||||
'linkable_id' => $report->id,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
// 결재선 단계 생성
|
||||
foreach ($steps as $index => $step) {
|
||||
ApprovalStep::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'approval_id' => $approval->id,
|
||||
'step_order' => $index + 1,
|
||||
'step_type' => $step['step_type'] ?? ApprovalLine::STEP_TYPE_APPROVAL,
|
||||
'approver_id' => $step['approver_id'],
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
]);
|
||||
}
|
||||
|
||||
// 보고서에 결재 연결
|
||||
$report->update([
|
||||
'approval_id' => $approval->id,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return $this->show($report->id);
|
||||
});
|
||||
}
|
||||
|
||||
// ── private ──
|
||||
|
||||
/**
|
||||
@@ -298,6 +378,26 @@ private function sumItemAmounts(array $items): int
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 문서번호 생성
|
||||
*/
|
||||
private function generateApprovalNumber(int $tenantId): string
|
||||
{
|
||||
$prefix = 'AP';
|
||||
$date = now()->format('Ymd');
|
||||
$pattern = "{$prefix}-{$date}-";
|
||||
|
||||
$lastNumber = Approval::withTrashed()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('document_number', 'like', "{$pattern}%")
|
||||
->orderByDesc('document_number')
|
||||
->value('document_number');
|
||||
|
||||
$seq = $lastNumber ? ((int) substr($lastNumber, -4) + 1) : 1;
|
||||
|
||||
return sprintf('%s-%s-%04d', $prefix, $date, $seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 전이 검증
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('nonconforming_reports', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('approval_id')->nullable()->after('status')->comment('결재 문서 ID');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('nonconforming_reports', function (Blueprint $table) {
|
||||
$table->dropColumn('approval_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -538,6 +538,8 @@
|
||||
'closed_cannot_delete' => '종결된 부적합 보고서는 삭제할 수 없습니다.',
|
||||
'invalid_status_transition' => "상태를 ':from'에서 ':to'(으)로 변경할 수 없습니다.",
|
||||
'analysis_required' => '조치완료로 변경하려면 원인 분석과 처리 방안을 먼저 입력해야 합니다.',
|
||||
'must_be_resolved_for_approval' => '조치완료 상태에서만 결재상신할 수 있습니다.',
|
||||
'approval_already_exists' => '이미 결재가 진행 중입니다.',
|
||||
],
|
||||
|
||||
// 데모 테넌트 관련
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
Route::put('/{id}', [NonconformingReportController::class, 'update'])->whereNumber('id')->name('v1.nonconforming-reports.update');
|
||||
Route::delete('/{id}', [NonconformingReportController::class, 'destroy'])->whereNumber('id')->name('v1.nonconforming-reports.destroy');
|
||||
Route::patch('/{id}/status', [NonconformingReportController::class, 'changeStatus'])->whereNumber('id')->name('v1.nonconforming-reports.change-status');
|
||||
Route::post('/{id}/submit-approval', [NonconformingReportController::class, 'submitApproval'])->whereNumber('id')->name('v1.nonconforming-reports.submit-approval');
|
||||
});
|
||||
|
||||
// Vehicle Dispatch API (배차차량 관리)
|
||||
|
||||
Reference in New Issue
Block a user