feat: [품질관리] 백엔드 API 구현 - 품질관리서 + 실적신고
- 품질관리서(quality_documents) CRUD API 14개 엔드포인트 - 실적신고(performance_reports) 관리 API 6개 엔드포인트 - DB 마이그레이션 4개 테이블 (quality_documents, quality_document_orders, quality_document_locations, performance_reports) - 모델 4개 + 서비스 2개 + 컨트롤러 2개 + FormRequest 4개 - stats() ambiguous column 버그 수정 (JOIN 시 테이블 접두사 추가) - missing() status_code 컬럼명/값 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
59
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
59
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Quality\PerformanceReportConfirmRequest;
|
||||
use App\Http\Requests\Quality\PerformanceReportMemoRequest;
|
||||
use App\Services\PerformanceReportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PerformanceReportController extends Controller
|
||||
{
|
||||
public function __construct(private PerformanceReportService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function stats(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->stats($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function confirm(PerformanceReportConfirmRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->confirm($request->validated()['ids']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function unconfirm(PerformanceReportConfirmRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->unconfirm($request->validated()['ids']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function updateMemo(PerformanceReportMemoRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->updateMemo($data['ids'], $data['memo']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function missing(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->missing($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
127
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
127
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Quality\QualityDocumentStoreRequest;
|
||||
use App\Http\Requests\Quality\QualityDocumentUpdateRequest;
|
||||
use App\Services\QualityDocumentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class QualityDocumentController extends Controller
|
||||
{
|
||||
public function __construct(private QualityDocumentService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function stats(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->stats($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function calendar(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->calendar($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function availableOrders(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->availableOrders($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function store(QualityDocumentStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
public function update(QualityDocumentUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->destroy($id);
|
||||
|
||||
return 'success';
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
public function complete(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->complete($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function attachOrders(Request $request, int $id)
|
||||
{
|
||||
$request->validate([
|
||||
'order_ids' => ['required', 'array', 'min:1'],
|
||||
'order_ids.*' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->attachOrders($id, $request->input('order_ids'));
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function detachOrder(int $id, int $orderId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $orderId) {
|
||||
return $this->service->detachOrder($id, $orderId);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function inspectLocation(Request $request, int $id, int $locId)
|
||||
{
|
||||
$request->validate([
|
||||
'post_width' => ['nullable', 'integer'],
|
||||
'post_height' => ['nullable', 'integer'],
|
||||
'change_reason' => ['nullable', 'string', 'max:500'],
|
||||
'inspection_status' => ['nullable', 'string', 'in:pending,completed'],
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request, $id, $locId) {
|
||||
return $this->service->inspectLocation($id, $locId, $request->all());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function requestDocument(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->requestDocument($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function resultDocument(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->resultDocument($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PerformanceReportConfirmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||
'ids.min' => __('validation.min.array', ['attribute' => '실적신고 ID', 'min' => 1]),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PerformanceReportMemoRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||
'memo' => ['required', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||
'memo.required' => __('validation.required', ['attribute' => '메모']),
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
43
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QualityDocumentStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'site_name' => ['required', 'string', 'max:200'],
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'received_date' => ['nullable', 'date'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.manager' => ['nullable', 'array'],
|
||||
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||
'options.inspection' => ['nullable', 'array'],
|
||||
'options.inspection.request_date' => ['nullable', 'date'],
|
||||
'options.inspection.start_date' => ['nullable', 'date'],
|
||||
'options.inspection.end_date' => ['nullable', 'date'],
|
||||
'options.site_address' => ['nullable', 'array'],
|
||||
'options.construction_site' => ['nullable', 'array'],
|
||||
'options.material_distributor' => ['nullable', 'array'],
|
||||
'options.contractor' => ['nullable', 'array'],
|
||||
'options.supervisor' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'site_name.required' => __('validation.required', ['attribute' => '현장명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
36
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QualityDocumentUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'site_name' => ['sometimes', 'string', 'max:200'],
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'received_date' => ['nullable', 'date'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.manager' => ['nullable', 'array'],
|
||||
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||
'options.inspection' => ['nullable', 'array'],
|
||||
'options.inspection.request_date' => ['nullable', 'date'],
|
||||
'options.inspection.start_date' => ['nullable', 'date'],
|
||||
'options.inspection.end_date' => ['nullable', 'date'],
|
||||
'options.site_address' => ['nullable', 'array'],
|
||||
'options.construction_site' => ['nullable', 'array'],
|
||||
'options.material_distributor' => ['nullable', 'array'],
|
||||
'options.contractor' => ['nullable', 'array'],
|
||||
'options.supervisor' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
76
app/Models/Qualitys/PerformanceReport.php
Normal file
76
app/Models/Qualitys/PerformanceReport.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PerformanceReport extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'performance_reports';
|
||||
|
||||
const STATUS_UNCONFIRMED = 'unconfirmed';
|
||||
|
||||
const STATUS_CONFIRMED = 'confirmed';
|
||||
|
||||
const STATUS_REPORTED = 'reported';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'quality_document_id',
|
||||
'year',
|
||||
'quarter',
|
||||
'confirmation_status',
|
||||
'confirmed_date',
|
||||
'confirmed_by',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'confirmed_date' => 'date',
|
||||
'year' => 'integer',
|
||||
'quarter' => 'integer',
|
||||
];
|
||||
|
||||
// ===== Relationships =====
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function confirmer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'confirmed_by');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// ===== Status Helpers =====
|
||||
|
||||
public function isUnconfirmed(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_UNCONFIRMED;
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_CONFIRMED;
|
||||
}
|
||||
|
||||
public function isReported(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_REPORTED;
|
||||
}
|
||||
}
|
||||
131
app/Models/Qualitys/QualityDocument.php
Normal file
131
app/Models/Qualitys/QualityDocument.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class QualityDocument extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'quality_documents';
|
||||
|
||||
const STATUS_RECEIVED = 'received';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'quality_doc_number',
|
||||
'site_name',
|
||||
'status',
|
||||
'client_id',
|
||||
'inspector_id',
|
||||
'received_date',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'options' => 'array',
|
||||
'received_date' => 'date',
|
||||
];
|
||||
|
||||
// ===== Relationships =====
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id');
|
||||
}
|
||||
|
||||
public function inspector()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'inspector_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function documentOrders()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentOrder::class);
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentLocation::class);
|
||||
}
|
||||
|
||||
public function performanceReport()
|
||||
{
|
||||
return $this->hasOne(PerformanceReport::class);
|
||||
}
|
||||
|
||||
// ===== 채번 =====
|
||||
|
||||
public static function generateDocNumber(int $tenantId): string
|
||||
{
|
||||
$prefix = 'KD-QD';
|
||||
$yearMonth = now()->format('Ym');
|
||||
|
||||
$lastNo = static::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('quality_doc_number', 'like', "{$prefix}-{$yearMonth}-%")
|
||||
->orderByDesc('quality_doc_number')
|
||||
->value('quality_doc_number');
|
||||
|
||||
if ($lastNo) {
|
||||
$seq = (int) substr($lastNo, -4) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
return sprintf('%s-%s-%04d', $prefix, $yearMonth, $seq);
|
||||
}
|
||||
|
||||
// ===== Status Helpers =====
|
||||
|
||||
public function isReceived(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_RECEIVED;
|
||||
}
|
||||
|
||||
public function isInProgress(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
public static function mapStatusToFrontend(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
self::STATUS_RECEIVED => 'reception',
|
||||
self::STATUS_IN_PROGRESS => 'in_progress',
|
||||
self::STATUS_COMPLETED => 'completed',
|
||||
default => $status,
|
||||
};
|
||||
}
|
||||
|
||||
public static function mapStatusFromFrontend(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'reception' => self::STATUS_RECEIVED,
|
||||
default => $status,
|
||||
};
|
||||
}
|
||||
}
|
||||
57
app/Models/Qualitys/QualityDocumentLocation.php
Normal file
57
app/Models/Qualitys/QualityDocumentLocation.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use App\Models\Orders\OrderItem;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class QualityDocumentLocation extends Model
|
||||
{
|
||||
protected $table = 'quality_document_locations';
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
protected $fillable = [
|
||||
'quality_document_id',
|
||||
'quality_document_order_id',
|
||||
'order_item_id',
|
||||
'post_width',
|
||||
'post_height',
|
||||
'change_reason',
|
||||
'document_id',
|
||||
'inspection_status',
|
||||
];
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function qualityDocumentOrder()
|
||||
{
|
||||
return $this->belongsTo(QualityDocumentOrder::class);
|
||||
}
|
||||
|
||||
public function orderItem()
|
||||
{
|
||||
return $this->belongsTo(OrderItem::class);
|
||||
}
|
||||
|
||||
public function document()
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->inspection_status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->inspection_status === self::STATUS_COMPLETED;
|
||||
}
|
||||
}
|
||||
31
app/Models/Qualitys/QualityDocumentOrder.php
Normal file
31
app/Models/Qualitys/QualityDocumentOrder.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class QualityDocumentOrder extends Model
|
||||
{
|
||||
protected $table = 'quality_document_orders';
|
||||
|
||||
protected $fillable = [
|
||||
'quality_document_id',
|
||||
'order_id',
|
||||
];
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentLocation::class);
|
||||
}
|
||||
}
|
||||
258
app/Services/PerformanceReportService.php
Normal file
258
app/Services/PerformanceReportService.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Qualitys\PerformanceReport;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class PerformanceReportService extends Service
|
||||
{
|
||||
private const AUDIT_TARGET = 'performance_report';
|
||||
|
||||
public function __construct(
|
||||
private readonly AuditLogger $auditLogger,
|
||||
private readonly QualityDocumentService $qualityDocumentService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
*/
|
||||
public function index(array $params)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$q = trim((string) ($params['q'] ?? ''));
|
||||
$year = $params['year'] ?? null;
|
||||
$quarter = $params['quarter'] ?? null;
|
||||
$confirmStatus = $params['confirm_status'] ?? null;
|
||||
|
||||
$query = PerformanceReport::query()
|
||||
->where('performance_reports.tenant_id', $tenantId)
|
||||
->with(['qualityDocument.client', 'qualityDocument.locations', 'confirmer:id,name']);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->whereHas('qualityDocument', function ($qq) use ($q) {
|
||||
$qq->where('quality_doc_number', 'like', "%{$q}%")
|
||||
->orWhere('site_name', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($year !== null) {
|
||||
$query->where('year', $year);
|
||||
}
|
||||
if ($quarter !== null) {
|
||||
$query->where('quarter', $quarter);
|
||||
}
|
||||
if ($confirmStatus !== null) {
|
||||
$query->where('confirmation_status', $confirmStatus);
|
||||
}
|
||||
|
||||
$query->orderByDesc('performance_reports.id');
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$transformedData = $paginated->getCollection()->map(fn ($report) => $this->transformToFrontend($report));
|
||||
|
||||
return [
|
||||
'items' => $transformedData,
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(array $params = []): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = PerformanceReport::where('performance_reports.tenant_id', $tenantId);
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->where('performance_reports.year', $params['year']);
|
||||
}
|
||||
if (! empty($params['quarter'])) {
|
||||
$query->where('performance_reports.quarter', $params['quarter']);
|
||||
}
|
||||
|
||||
$counts = (clone $query)
|
||||
->select('confirmation_status', DB::raw('count(*) as count'))
|
||||
->groupBy('confirmation_status')
|
||||
->pluck('count', 'confirmation_status')
|
||||
->toArray();
|
||||
|
||||
$totalLocations = (clone $query)
|
||||
->join('quality_documents', 'quality_documents.id', '=', 'performance_reports.quality_document_id')
|
||||
->join('quality_document_locations', 'quality_document_locations.quality_document_id', '=', 'quality_documents.id')
|
||||
->count('quality_document_locations.id');
|
||||
|
||||
return [
|
||||
'total_count' => array_sum($counts),
|
||||
'confirmed_count' => $counts[PerformanceReport::STATUS_CONFIRMED] ?? 0,
|
||||
'unconfirmed_count' => $counts[PerformanceReport::STATUS_UNCONFIRMED] ?? 0,
|
||||
'total_locations' => $totalLocations,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 확정
|
||||
*/
|
||||
public function confirm(array $ids)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $tenantId, $userId) {
|
||||
$reports = PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->with(['qualityDocument'])
|
||||
->get();
|
||||
|
||||
$errors = [];
|
||||
foreach ($reports as $report) {
|
||||
if ($report->isConfirmed() || $report->isReported()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 필수정보 검증
|
||||
$requiredInfo = $this->qualityDocumentService->calculateRequiredInfo($report->qualityDocument);
|
||||
if ($requiredInfo !== '완료') {
|
||||
$errors[] = [
|
||||
'id' => $report->id,
|
||||
'quality_doc_number' => $report->qualityDocument->quality_doc_number,
|
||||
'reason' => $requiredInfo,
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$report->update([
|
||||
'confirmation_status' => PerformanceReport::STATUS_CONFIRMED,
|
||||
'confirmed_date' => now()->toDateString(),
|
||||
'confirmed_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
throw new BadRequestHttpException(json_encode([
|
||||
'message' => __('error.quality.confirm_failed'),
|
||||
'errors' => $errors,
|
||||
]));
|
||||
}
|
||||
|
||||
return ['confirmed_count' => count($ids) - count($errors)];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 확정 해제
|
||||
*/
|
||||
public function unconfirm(array $ids)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $tenantId, $userId) {
|
||||
PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->where('confirmation_status', PerformanceReport::STATUS_CONFIRMED)
|
||||
->update([
|
||||
'confirmation_status' => PerformanceReport::STATUS_UNCONFIRMED,
|
||||
'confirmed_date' => null,
|
||||
'confirmed_by' => null,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ['unconfirmed_count' => count($ids)];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 메모 업데이트
|
||||
*/
|
||||
public function updateMemo(array $ids, string $memo)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'memo' => $memo,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ['updated_count' => count($ids)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 누락체크 (출고완료 but 제품검사 미등록)
|
||||
*/
|
||||
public function missing(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 품질관리서가 등록된 수주 ID
|
||||
$registeredOrderIds = DB::table('quality_document_orders')
|
||||
->join('quality_documents', 'quality_documents.id', '=', 'quality_document_orders.quality_document_id')
|
||||
->where('quality_documents.tenant_id', $tenantId)
|
||||
->pluck('quality_document_orders.order_id');
|
||||
|
||||
// 출고완료 상태이지만 품질관리서 미등록 수주
|
||||
$query = DB::table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotIn('id', $registeredOrderIds)
|
||||
->where('status_code', 'SHIPPED'); // TODO: 출고완료 상태 추가 시 상수 확인
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->whereYear('created_at', $params['year']);
|
||||
}
|
||||
if (! empty($params['quarter'])) {
|
||||
$quarter = (int) $params['quarter'];
|
||||
$startMonth = ($quarter - 1) * 3 + 1;
|
||||
$endMonth = $quarter * 3;
|
||||
$query->whereMonth('created_at', '>=', $startMonth)
|
||||
->whereMonth('created_at', '<=', $endMonth);
|
||||
}
|
||||
|
||||
return $query->orderByDesc('id')
|
||||
->limit(100)
|
||||
->get()
|
||||
->map(fn ($order) => [
|
||||
'id' => $order->id,
|
||||
'order_number' => $order->order_no ?? '',
|
||||
'site_name' => $order->site_name ?? '',
|
||||
'client' => '', // 별도 조인 필요
|
||||
'delivery_date' => $order->delivery_date ?? '',
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* DB → 프론트엔드 변환
|
||||
*/
|
||||
private function transformToFrontend(PerformanceReport $report): array
|
||||
{
|
||||
$doc = $report->qualityDocument;
|
||||
|
||||
return [
|
||||
'id' => $report->id,
|
||||
'quality_doc_number' => $doc?->quality_doc_number ?? '',
|
||||
'created_date' => $report->created_at?->format('Y-m-d') ?? '',
|
||||
'site_name' => $doc?->site_name ?? '',
|
||||
'client' => $doc?->client?->name ?? '',
|
||||
'location_count' => $doc?->locations?->count() ?? 0,
|
||||
'required_info' => $doc ? $this->qualityDocumentService->calculateRequiredInfo($doc) : '',
|
||||
'confirm_status' => $report->confirmation_status === PerformanceReport::STATUS_CONFIRMED ? 'confirmed' : 'unconfirmed',
|
||||
'confirm_date' => $report->confirmed_date?->format('Y-m-d'),
|
||||
'memo' => $report->memo ?? '',
|
||||
'year' => $report->year,
|
||||
'quarter' => $report->quarter,
|
||||
];
|
||||
}
|
||||
}
|
||||
748
app/Services/QualityDocumentService.php
Normal file
748
app/Services/QualityDocumentService.php
Normal file
@@ -0,0 +1,748 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Orders\OrderItem;
|
||||
use App\Models\Qualitys\PerformanceReport;
|
||||
use App\Models\Qualitys\QualityDocument;
|
||||
use App\Models\Qualitys\QualityDocumentLocation;
|
||||
use App\Models\Qualitys\QualityDocumentOrder;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class QualityDocumentService extends Service
|
||||
{
|
||||
private const AUDIT_TARGET = 'quality_document';
|
||||
|
||||
public function __construct(
|
||||
private readonly AuditLogger $auditLogger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
*/
|
||||
public function index(array $params)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$q = trim((string) ($params['q'] ?? ''));
|
||||
$status = $params['status'] ?? null;
|
||||
$dateFrom = $params['date_from'] ?? null;
|
||||
$dateTo = $params['date_to'] ?? null;
|
||||
|
||||
$query = QualityDocument::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations']);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where(function ($qq) use ($q) {
|
||||
$qq->where('quality_doc_number', 'like', "%{$q}%")
|
||||
->orWhere('site_name', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status !== null) {
|
||||
$dbStatus = QualityDocument::mapStatusFromFrontend($status);
|
||||
$query->where('status', $dbStatus);
|
||||
}
|
||||
|
||||
if ($dateFrom !== null) {
|
||||
$query->where('received_date', '>=', $dateFrom);
|
||||
}
|
||||
if ($dateTo !== null) {
|
||||
$query->where('received_date', '<=', $dateTo);
|
||||
}
|
||||
|
||||
$query->orderByDesc('id');
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$transformedData = $paginated->getCollection()->map(fn ($doc) => $this->transformToFrontend($doc));
|
||||
|
||||
return [
|
||||
'items' => $transformedData,
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(array $params = []): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = QualityDocument::where('tenant_id', $tenantId);
|
||||
|
||||
if (! empty($params['date_from'])) {
|
||||
$query->where('received_date', '>=', $params['date_from']);
|
||||
}
|
||||
if (! empty($params['date_to'])) {
|
||||
$query->where('received_date', '<=', $params['date_to']);
|
||||
}
|
||||
|
||||
$counts = (clone $query)
|
||||
->select('status', DB::raw('count(*) as count'))
|
||||
->groupBy('status')
|
||||
->pluck('count', 'status')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'reception_count' => $counts[QualityDocument::STATUS_RECEIVED] ?? 0,
|
||||
'in_progress_count' => $counts[QualityDocument::STATUS_IN_PROGRESS] ?? 0,
|
||||
'completed_count' => $counts[QualityDocument::STATUS_COMPLETED] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 캘린더 스케줄 조회
|
||||
*/
|
||||
public function calendar(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$year = (int) ($params['year'] ?? now()->year);
|
||||
$month = (int) ($params['month'] ?? now()->month);
|
||||
|
||||
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
$query = QualityDocument::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['inspector:id,name']);
|
||||
|
||||
// options JSON 내 inspection.start_date / inspection.end_date 기준 필터링
|
||||
// received_date 기준으로 범위 필터 (options JSON은 직접 SQL 필터 어려우므로)
|
||||
$query->where(function ($q) use ($startDate, $endDate) {
|
||||
$q->whereBetween('received_date', [$startDate, $endDate]);
|
||||
});
|
||||
|
||||
if (! empty($params['status'])) {
|
||||
$dbStatus = QualityDocument::mapStatusFromFrontend($params['status']);
|
||||
$query->where('status', $dbStatus);
|
||||
}
|
||||
|
||||
return $query->orderBy('received_date')
|
||||
->get()
|
||||
->map(function (QualityDocument $doc) {
|
||||
$options = $doc->options ?? [];
|
||||
$inspection = $options['inspection'] ?? [];
|
||||
|
||||
return [
|
||||
'id' => $doc->id,
|
||||
'start_date' => $inspection['start_date'] ?? $doc->received_date?->format('Y-m-d'),
|
||||
'end_date' => $inspection['end_date'] ?? $doc->received_date?->format('Y-m-d'),
|
||||
'inspector' => $doc->inspector?->name ?? '',
|
||||
'site_name' => $doc->site_name,
|
||||
'status' => QualityDocument::mapStatusToFrontend($doc->status),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 조회
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client',
|
||||
'inspector:id,name',
|
||||
'creator:id,name',
|
||||
'documentOrders.order',
|
||||
'locations.orderItem.node',
|
||||
])
|
||||
->find($id);
|
||||
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return $this->transformToFrontend($doc, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성
|
||||
*/
|
||||
public function store(array $data)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$data['tenant_id'] = $tenantId;
|
||||
$data['quality_doc_number'] = QualityDocument::generateDocNumber($tenantId);
|
||||
$data['status'] = QualityDocument::STATUS_RECEIVED;
|
||||
$data['created_by'] = $userId;
|
||||
|
||||
$doc = QualityDocument::create($data);
|
||||
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$doc->id,
|
||||
'created',
|
||||
null,
|
||||
$doc->toArray()
|
||||
);
|
||||
|
||||
return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name']));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(int $id, array $data)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)->find($id);
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$beforeData = $doc->toArray();
|
||||
|
||||
return DB::transaction(function () use ($doc, $data, $userId, $beforeData) {
|
||||
$data['updated_by'] = $userId;
|
||||
|
||||
// options는 기존 값과 병합
|
||||
if (isset($data['options'])) {
|
||||
$existingOptions = $doc->options ?? [];
|
||||
$data['options'] = array_replace_recursive($existingOptions, $data['options']);
|
||||
}
|
||||
|
||||
$doc->update($data);
|
||||
|
||||
$this->auditLogger->log(
|
||||
$doc->tenant_id,
|
||||
self::AUDIT_TARGET,
|
||||
$doc->id,
|
||||
'updated',
|
||||
$beforeData,
|
||||
$doc->fresh()->toArray()
|
||||
);
|
||||
|
||||
return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations']));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)->find($id);
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
if ($doc->isCompleted()) {
|
||||
throw new BadRequestHttpException(__('error.quality.cannot_delete_completed'));
|
||||
}
|
||||
|
||||
$beforeData = $doc->toArray();
|
||||
$doc->deleted_by = $this->apiUserId();
|
||||
$doc->save();
|
||||
$doc->delete();
|
||||
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$doc->id,
|
||||
'deleted',
|
||||
$beforeData,
|
||||
null
|
||||
);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 검사 완료 처리
|
||||
*/
|
||||
public function complete(int $id)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)
|
||||
->with(['locations'])
|
||||
->find($id);
|
||||
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
if ($doc->isCompleted()) {
|
||||
throw new BadRequestHttpException(__('error.quality.already_completed'));
|
||||
}
|
||||
|
||||
// 미완료 개소 확인
|
||||
$pendingCount = $doc->locations->where('inspection_status', QualityDocumentLocation::STATUS_PENDING)->count();
|
||||
if ($pendingCount > 0) {
|
||||
throw new BadRequestHttpException(__('error.quality.pending_locations', ['count' => $pendingCount]));
|
||||
}
|
||||
|
||||
$beforeData = $doc->toArray();
|
||||
|
||||
return DB::transaction(function () use ($doc, $userId, $tenantId, $beforeData) {
|
||||
$doc->update([
|
||||
'status' => QualityDocument::STATUS_COMPLETED,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
// 실적신고 자동 생성
|
||||
$now = now();
|
||||
PerformanceReport::firstOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'quality_document_id' => $doc->id,
|
||||
],
|
||||
[
|
||||
'year' => $now->year,
|
||||
'quarter' => (int) ceil($now->month / 3),
|
||||
'confirmation_status' => PerformanceReport::STATUS_UNCONFIRMED,
|
||||
'created_by' => $userId,
|
||||
]
|
||||
);
|
||||
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$doc->id,
|
||||
'completed',
|
||||
$beforeData,
|
||||
$doc->fresh()->toArray()
|
||||
);
|
||||
|
||||
return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name']));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 검사 미등록 수주 목록
|
||||
*/
|
||||
public function availableOrders(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$q = trim((string) ($params['q'] ?? ''));
|
||||
|
||||
// 이미 연결된 수주 ID 목록
|
||||
$linkedOrderIds = QualityDocumentOrder::whereHas('qualityDocument', function ($query) use ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
})->pluck('order_id');
|
||||
|
||||
$query = Order::where('tenant_id', $tenantId)
|
||||
->whereNotIn('id', $linkedOrderIds)
|
||||
->with(['items'])
|
||||
->withCount('items');
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where(function ($qq) use ($q) {
|
||||
$qq->where('order_no', 'like', "%{$q}%")
|
||||
->orWhere('site_name', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query->orderByDesc('id')
|
||||
->limit(50)
|
||||
->get()
|
||||
->map(fn ($order) => [
|
||||
'id' => $order->id,
|
||||
'order_number' => $order->order_no,
|
||||
'site_name' => $order->site_name ?? '',
|
||||
'delivery_date' => $order->delivery_date ?? '',
|
||||
'location_count' => $order->items_count,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 연결
|
||||
*/
|
||||
public function attachOrders(int $docId, array $orderIds)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)->find($docId);
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($doc, $orderIds, $tenantId) {
|
||||
foreach ($orderIds as $orderId) {
|
||||
$order = Order::where('tenant_id', $tenantId)->find($orderId);
|
||||
if (! $order) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
$docOrder = QualityDocumentOrder::firstOrCreate([
|
||||
'quality_document_id' => $doc->id,
|
||||
'order_id' => $orderId,
|
||||
]);
|
||||
|
||||
// 수주 연결 시 개소(order_items)를 locations에 자동 생성
|
||||
$orderItems = OrderItem::where('order_id', $orderId)->get();
|
||||
foreach ($orderItems as $item) {
|
||||
QualityDocumentLocation::firstOrCreate([
|
||||
'quality_document_id' => $doc->id,
|
||||
'quality_document_order_id' => $docOrder->id,
|
||||
'order_item_id' => $item->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 상태를 진행중으로 변경 (접수 상태일 때)
|
||||
if ($doc->isReceived()) {
|
||||
$doc->update(['status' => QualityDocument::STATUS_IN_PROGRESS]);
|
||||
}
|
||||
|
||||
return $this->transformToFrontend(
|
||||
$doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations.orderItem.node'])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 연결 해제
|
||||
*/
|
||||
public function detachOrder(int $docId, int $orderId)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)->find($docId);
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
if ($doc->isCompleted()) {
|
||||
throw new BadRequestHttpException(__('error.quality.cannot_modify_completed'));
|
||||
}
|
||||
|
||||
$docOrder = QualityDocumentOrder::where('quality_document_id', $docId)
|
||||
->where('order_id', $orderId)
|
||||
->first();
|
||||
|
||||
if ($docOrder) {
|
||||
// 해당 수주의 locations 삭제
|
||||
QualityDocumentLocation::where('quality_document_order_id', $docOrder->id)->delete();
|
||||
$docOrder->delete();
|
||||
}
|
||||
|
||||
return $this->transformToFrontend(
|
||||
$doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations.orderItem.node'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 필수정보 계산
|
||||
*/
|
||||
public function calculateRequiredInfo(QualityDocument $doc): string
|
||||
{
|
||||
$options = $doc->options ?? [];
|
||||
$missing = 0;
|
||||
|
||||
$sections = [
|
||||
'construction_site' => ['name', 'land_location', 'lot_number'],
|
||||
'material_distributor' => ['company', 'address', 'ceo', 'phone'],
|
||||
'contractor' => ['company', 'address', 'name', 'phone'],
|
||||
'supervisor' => ['office', 'address', 'name', 'phone'],
|
||||
];
|
||||
|
||||
foreach ($sections as $section => $fields) {
|
||||
$data = $options[$section] ?? [];
|
||||
foreach ($fields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
$missing++;
|
||||
break; // 섹션 단위
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $missing === 0 ? '완료' : "{$missing}건 누락";
|
||||
}
|
||||
|
||||
/**
|
||||
* DB → 프론트엔드 변환
|
||||
*/
|
||||
private function transformToFrontend(QualityDocument $doc, bool $detail = false): array
|
||||
{
|
||||
$options = $doc->options ?? [];
|
||||
|
||||
$result = [
|
||||
'id' => $doc->id,
|
||||
'quality_doc_number' => $doc->quality_doc_number,
|
||||
'site_name' => $doc->site_name,
|
||||
'client' => $doc->client?->name ?? '',
|
||||
'location_count' => $doc->locations?->count() ?? 0,
|
||||
'required_info' => $this->calculateRequiredInfo($doc),
|
||||
'inspection_period' => $this->formatInspectionPeriod($options),
|
||||
'inspector' => $doc->inspector?->name ?? '',
|
||||
'status' => QualityDocument::mapStatusToFrontend($doc->status),
|
||||
'author' => $doc->creator?->name ?? '',
|
||||
'reception_date' => $doc->received_date?->format('Y-m-d'),
|
||||
'manager' => $options['manager']['name'] ?? '',
|
||||
'manager_contact' => $options['manager']['phone'] ?? '',
|
||||
];
|
||||
|
||||
if ($detail) {
|
||||
$result['construction_site'] = [
|
||||
'site_name' => $options['construction_site']['name'] ?? '',
|
||||
'land_location' => $options['construction_site']['land_location'] ?? '',
|
||||
'lot_number' => $options['construction_site']['lot_number'] ?? '',
|
||||
];
|
||||
$result['material_distributor'] = [
|
||||
'company_name' => $options['material_distributor']['company'] ?? '',
|
||||
'company_address' => $options['material_distributor']['address'] ?? '',
|
||||
'representative_name' => $options['material_distributor']['ceo'] ?? '',
|
||||
'phone' => $options['material_distributor']['phone'] ?? '',
|
||||
];
|
||||
$result['constructor_info'] = [
|
||||
'company_name' => $options['contractor']['company'] ?? '',
|
||||
'company_address' => $options['contractor']['address'] ?? '',
|
||||
'name' => $options['contractor']['name'] ?? '',
|
||||
'phone' => $options['contractor']['phone'] ?? '',
|
||||
];
|
||||
$result['supervisor'] = [
|
||||
'office_name' => $options['supervisor']['office'] ?? '',
|
||||
'office_address' => $options['supervisor']['address'] ?? '',
|
||||
'name' => $options['supervisor']['name'] ?? '',
|
||||
'phone' => $options['supervisor']['phone'] ?? '',
|
||||
];
|
||||
$result['schedule_info'] = [
|
||||
'visit_request_date' => $options['inspection']['request_date'] ?? '',
|
||||
'start_date' => $options['inspection']['start_date'] ?? '',
|
||||
'end_date' => $options['inspection']['end_date'] ?? '',
|
||||
'inspector' => $doc->inspector?->name ?? '',
|
||||
'site_postal_code' => $options['site_address']['postal_code'] ?? '',
|
||||
'site_address' => $options['site_address']['address'] ?? '',
|
||||
'site_address_detail' => $options['site_address']['detail'] ?? '',
|
||||
];
|
||||
|
||||
// 개소 목록
|
||||
$result['order_items'] = $doc->locations->map(function ($loc) {
|
||||
$orderItem = $loc->orderItem;
|
||||
$node = $orderItem?->node;
|
||||
$nodeOptions = $node?->options ?? [];
|
||||
$order = $loc->qualityDocumentOrder?->order;
|
||||
|
||||
return [
|
||||
'id' => (string) $loc->id,
|
||||
'order_number' => $order?->order_no ?? '',
|
||||
'site_name' => $order?->site_name ?? '',
|
||||
'delivery_date' => $order?->delivery_date ?? '',
|
||||
'floor' => $orderItem?->floor_code ?? '',
|
||||
'symbol' => $orderItem?->symbol_code ?? '',
|
||||
'order_width' => $nodeOptions['width'] ?? 0,
|
||||
'order_height' => $nodeOptions['height'] ?? 0,
|
||||
'construction_width' => $loc->post_width ?? 0,
|
||||
'construction_height' => $loc->post_height ?? 0,
|
||||
'change_reason' => $loc->change_reason ?? '',
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개소별 검사 저장 (시공후 규격 + 검사 성적서)
|
||||
*/
|
||||
public function inspectLocation(int $docId, int $locId, array $data)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)->find($docId);
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
if ($doc->isCompleted()) {
|
||||
throw new BadRequestHttpException(__('error.quality.cannot_modify_completed'));
|
||||
}
|
||||
|
||||
$location = QualityDocumentLocation::where('quality_document_id', $docId)->find($locId);
|
||||
if (! $location) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($location, $data, $doc) {
|
||||
$updateData = [];
|
||||
|
||||
if (isset($data['post_width'])) {
|
||||
$updateData['post_width'] = $data['post_width'];
|
||||
}
|
||||
if (isset($data['post_height'])) {
|
||||
$updateData['post_height'] = $data['post_height'];
|
||||
}
|
||||
if (isset($data['change_reason'])) {
|
||||
$updateData['change_reason'] = $data['change_reason'];
|
||||
}
|
||||
if (isset($data['inspection_status'])) {
|
||||
$updateData['inspection_status'] = $data['inspection_status'];
|
||||
}
|
||||
|
||||
if (! empty($updateData)) {
|
||||
$location->update($updateData);
|
||||
}
|
||||
|
||||
// 상태를 진행중으로 변경 (접수 상태일 때)
|
||||
if ($doc->isReceived()) {
|
||||
$doc->update(['status' => QualityDocument::STATUS_IN_PROGRESS]);
|
||||
}
|
||||
|
||||
return $location->fresh()->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 검사제품요청서 데이터 (PDF/프린트용)
|
||||
*/
|
||||
public function requestDocument(int $id): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client',
|
||||
'inspector:id,name',
|
||||
'documentOrders.order',
|
||||
'locations.orderItem.node',
|
||||
])
|
||||
->find($id);
|
||||
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$options = $doc->options ?? [];
|
||||
|
||||
return [
|
||||
'quality_doc_number' => $doc->quality_doc_number,
|
||||
'site_name' => $doc->site_name,
|
||||
'client' => $doc->client?->name ?? '',
|
||||
'received_date' => $doc->received_date?->format('Y-m-d'),
|
||||
'inspector' => $doc->inspector?->name ?? '',
|
||||
'construction_site' => $options['construction_site'] ?? [],
|
||||
'material_distributor' => $options['material_distributor'] ?? [],
|
||||
'contractor' => $options['contractor'] ?? [],
|
||||
'supervisor' => $options['supervisor'] ?? [],
|
||||
'inspection' => $options['inspection'] ?? [],
|
||||
'site_address' => $options['site_address'] ?? [],
|
||||
'manager' => $options['manager'] ?? [],
|
||||
'items' => $doc->locations->map(function ($loc) {
|
||||
$orderItem = $loc->orderItem;
|
||||
$node = $orderItem?->node;
|
||||
$nodeOptions = $node?->options ?? [];
|
||||
$order = $loc->qualityDocumentOrder?->order;
|
||||
|
||||
return [
|
||||
'order_number' => $order?->order_no ?? '',
|
||||
'floor' => $orderItem?->floor_code ?? '',
|
||||
'symbol' => $orderItem?->symbol_code ?? '',
|
||||
'item_name' => $orderItem?->item_name ?? '',
|
||||
'specification' => $orderItem?->specification ?? '',
|
||||
'order_width' => $nodeOptions['width'] ?? 0,
|
||||
'order_height' => $nodeOptions['height'] ?? 0,
|
||||
'quantity' => $orderItem?->quantity ?? 1,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 제품검사성적서 데이터 (documents EAV 연동)
|
||||
*/
|
||||
public function resultDocument(int $id): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = QualityDocument::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client',
|
||||
'inspector:id,name',
|
||||
'locations.orderItem.node',
|
||||
'locations.document.data',
|
||||
'locations.document.template',
|
||||
])
|
||||
->find($id);
|
||||
|
||||
if (! $doc) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$options = $doc->options ?? [];
|
||||
|
||||
return [
|
||||
'quality_doc_number' => $doc->quality_doc_number,
|
||||
'site_name' => $doc->site_name,
|
||||
'client' => $doc->client?->name ?? '',
|
||||
'inspector' => $doc->inspector?->name ?? '',
|
||||
'status' => QualityDocument::mapStatusToFrontend($doc->status),
|
||||
'locations' => $doc->locations->map(function ($loc) {
|
||||
$orderItem = $loc->orderItem;
|
||||
$node = $orderItem?->node;
|
||||
$nodeOptions = $node?->options ?? [];
|
||||
$document = $loc->document;
|
||||
|
||||
$result = [
|
||||
'id' => $loc->id,
|
||||
'floor' => $orderItem?->floor_code ?? '',
|
||||
'symbol' => $orderItem?->symbol_code ?? '',
|
||||
'order_width' => $nodeOptions['width'] ?? 0,
|
||||
'order_height' => $nodeOptions['height'] ?? 0,
|
||||
'post_width' => $loc->post_width,
|
||||
'post_height' => $loc->post_height,
|
||||
'change_reason' => $loc->change_reason,
|
||||
'inspection_status' => $loc->inspection_status,
|
||||
'document_id' => $loc->document_id,
|
||||
];
|
||||
|
||||
// EAV 문서 데이터가 있으면 포함
|
||||
if ($document) {
|
||||
$result['document'] = [
|
||||
'id' => $document->id,
|
||||
'document_no' => $document->document_no,
|
||||
'status' => $document->status,
|
||||
'template_id' => $document->template_id,
|
||||
'data' => $document->data?->map(fn ($d) => [
|
||||
'field_key' => $d->field_key,
|
||||
'field_value' => $d->field_value,
|
||||
'section_id' => $d->section_id,
|
||||
'column_id' => $d->column_id,
|
||||
])->toArray() ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatInspectionPeriod(array $options): string
|
||||
{
|
||||
$inspection = $options['inspection'] ?? [];
|
||||
$start = $inspection['start_date'] ?? '';
|
||||
$end = $inspection['end_date'] ?? '';
|
||||
|
||||
if ($start && $end) {
|
||||
return "{$start}~{$end}";
|
||||
}
|
||||
|
||||
return $start ?: $end ?: '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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::create('quality_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained();
|
||||
$table->string('quality_doc_number', 30)->comment('품질관리서 번호');
|
||||
$table->string('site_name')->comment('현장명');
|
||||
$table->string('status', 20)->default('received')->comment('received/in_progress/completed');
|
||||
$table->foreignId('client_id')->nullable()->constrained('clients')->comment('수주처');
|
||||
$table->foreignId('inspector_id')->nullable()->constrained('users')->comment('검사자');
|
||||
$table->date('received_date')->nullable()->comment('접수일');
|
||||
$table->json('options')->nullable()->comment('관련자정보, 검사정보, 현장주소 등');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'quality_doc_number']);
|
||||
$table->index(['tenant_id', 'status']);
|
||||
$table->index(['tenant_id', 'client_id']);
|
||||
$table->index(['tenant_id', 'inspector_id']);
|
||||
$table->index(['tenant_id', 'received_date']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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::create('quality_document_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('order_id')->constrained('orders');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['quality_document_id', 'order_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_document_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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::create('quality_document_locations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('quality_document_order_id')->constrained('quality_document_orders', 'id', 'qdl_qdo_id_fk')->cascadeOnDelete();
|
||||
$table->foreignId('order_item_id')->constrained('order_items');
|
||||
$table->integer('post_width')->nullable()->comment('시공후 가로');
|
||||
$table->integer('post_height')->nullable()->comment('시공후 세로');
|
||||
$table->string('change_reason')->nullable()->comment('규격 변경사유');
|
||||
$table->foreignId('document_id')->nullable()->comment('검사성적서 문서 ID');
|
||||
$table->string('inspection_status', 20)->default('pending')->comment('pending/completed');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['quality_document_id', 'inspection_status'], 'qdl_doc_id_status_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_document_locations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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::create('performance_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained();
|
||||
$table->foreignId('quality_document_id')->constrained();
|
||||
$table->unsignedSmallInteger('year')->comment('연도');
|
||||
$table->unsignedTinyInteger('quarter')->comment('분기 1-4');
|
||||
$table->string('confirmation_status', 20)->default('unconfirmed')->comment('unconfirmed/confirmed/reported');
|
||||
$table->date('confirmed_date')->nullable();
|
||||
$table->foreignId('confirmed_by')->nullable()->constrained('users');
|
||||
$table->text('memo')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'quality_document_id']);
|
||||
$table->index(['tenant_id', 'year', 'quarter']);
|
||||
$table->index(['tenant_id', 'confirmation_status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('performance_reports');
|
||||
}
|
||||
};
|
||||
@@ -444,6 +444,15 @@
|
||||
'already_completed' => '이미 완료된 검사입니다.',
|
||||
],
|
||||
|
||||
// 품질관리서 관련
|
||||
'quality' => [
|
||||
'cannot_delete_completed' => '완료된 품질관리서는 삭제할 수 없습니다.',
|
||||
'already_completed' => '이미 완료된 품질관리서입니다.',
|
||||
'cannot_modify_completed' => '완료된 품질관리서는 수정할 수 없습니다.',
|
||||
'pending_locations' => '미완료 개소가 :count건 있습니다.',
|
||||
'confirm_failed' => '필수정보가 누락된 건이 있어 확정할 수 없습니다.',
|
||||
],
|
||||
|
||||
// 입찰 관련
|
||||
'bidding' => [
|
||||
'not_found' => '입찰을 찾을 수 없습니다.',
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
require __DIR__.'/api/v1/app.php';
|
||||
require __DIR__.'/api/v1/audit.php';
|
||||
require __DIR__.'/api/v1/esign.php';
|
||||
require __DIR__.'/api/v1/quality.php';
|
||||
|
||||
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
||||
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
||||
|
||||
40
routes/api/v1/quality.php
Normal file
40
routes/api/v1/quality.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 품질관리 API 라우트 (v1)
|
||||
*
|
||||
* - 제품검사 (품질관리서)
|
||||
* - 실적신고
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Api\V1\PerformanceReportController;
|
||||
use App\Http\Controllers\Api\V1\QualityDocumentController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// 제품검사 (품질관리서)
|
||||
Route::prefix('quality/documents')->group(function () {
|
||||
Route::get('', [QualityDocumentController::class, 'index'])->name('v1.quality.documents.index');
|
||||
Route::get('/stats', [QualityDocumentController::class, 'stats'])->name('v1.quality.documents.stats');
|
||||
Route::get('/calendar', [QualityDocumentController::class, 'calendar'])->name('v1.quality.documents.calendar');
|
||||
Route::get('/available-orders', [QualityDocumentController::class, 'availableOrders'])->name('v1.quality.documents.available-orders');
|
||||
Route::post('', [QualityDocumentController::class, 'store'])->name('v1.quality.documents.store');
|
||||
Route::get('/{id}', [QualityDocumentController::class, 'show'])->whereNumber('id')->name('v1.quality.documents.show');
|
||||
Route::put('/{id}', [QualityDocumentController::class, 'update'])->whereNumber('id')->name('v1.quality.documents.update');
|
||||
Route::delete('/{id}', [QualityDocumentController::class, 'destroy'])->whereNumber('id')->name('v1.quality.documents.destroy');
|
||||
Route::patch('/{id}/complete', [QualityDocumentController::class, 'complete'])->whereNumber('id')->name('v1.quality.documents.complete');
|
||||
Route::post('/{id}/orders', [QualityDocumentController::class, 'attachOrders'])->whereNumber('id')->name('v1.quality.documents.attach-orders');
|
||||
Route::delete('/{id}/orders/{orderId}', [QualityDocumentController::class, 'detachOrder'])->whereNumber('id')->whereNumber('orderId')->name('v1.quality.documents.detach-order');
|
||||
Route::post('/{id}/locations/{locId}/inspect', [QualityDocumentController::class, 'inspectLocation'])->whereNumber('id')->whereNumber('locId')->name('v1.quality.documents.inspect-location');
|
||||
Route::get('/{id}/request-document', [QualityDocumentController::class, 'requestDocument'])->whereNumber('id')->name('v1.quality.documents.request-document');
|
||||
Route::get('/{id}/result-document', [QualityDocumentController::class, 'resultDocument'])->whereNumber('id')->name('v1.quality.documents.result-document');
|
||||
});
|
||||
|
||||
// 실적신고
|
||||
Route::prefix('quality/performance-reports')->group(function () {
|
||||
Route::get('', [PerformanceReportController::class, 'index'])->name('v1.quality.performance-reports.index');
|
||||
Route::get('/stats', [PerformanceReportController::class, 'stats'])->name('v1.quality.performance-reports.stats');
|
||||
Route::get('/missing', [PerformanceReportController::class, 'missing'])->name('v1.quality.performance-reports.missing');
|
||||
Route::patch('/confirm', [PerformanceReportController::class, 'confirm'])->name('v1.quality.performance-reports.confirm');
|
||||
Route::patch('/unconfirm', [PerformanceReportController::class, 'unconfirm'])->name('v1.quality.performance-reports.unconfirm');
|
||||
Route::patch('/memo', [PerformanceReportController::class, 'updateMemo'])->name('v1.quality.performance-reports.memo');
|
||||
});
|
||||
Reference in New Issue
Block a user