feat: [approval] Phase 2 결재관리 고급 기능 구현
- 보류/해제: 현재 결재자가 문서를 보류하고 해제 - 전결: 이후 모든 결재를 건너뛰고 최종 승인 - 회수 강화: 회수 사유 입력, 첫 결재자 미처리 시에만 허용 - 복사 재기안: 완료/반려/회수 문서를 복사하여 새 draft 생성 - 참조 열람 추적: 미열람/열람 필터, mark-read API - ApprovalDelegation 모델 생성 (Phase 3 위임 대결 준비) - 뱃지 카운트에 reference_unread 추가
This commit is contained in:
@@ -65,7 +65,7 @@ public function references(Request $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->getReferencesForMe(
|
||||
auth()->id(),
|
||||
$request->only(['search', 'date_from', 'date_to']),
|
||||
$request->only(['search', 'date_from', 'date_to', 'is_read']),
|
||||
(int) $request->get('per_page', 15)
|
||||
);
|
||||
|
||||
@@ -234,10 +234,10 @@ public function reject(Request $request, int $id): JsonResponse
|
||||
/**
|
||||
* 회수
|
||||
*/
|
||||
public function cancel(int $id): JsonResponse
|
||||
public function cancel(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$approval = $this->service->cancel($id);
|
||||
$approval = $this->service->cancel($id, $request->get('recall_reason'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -252,6 +252,107 @@ public function cancel(int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류
|
||||
*/
|
||||
public function hold(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'comment' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
try {
|
||||
$approval = $this->service->hold($id, $request->get('comment'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '보류되었습니다.',
|
||||
'data' => $approval,
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 해제
|
||||
*/
|
||||
public function releaseHold(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$approval = $this->service->releaseHold($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '보류가 해제되었습니다.',
|
||||
'data' => $approval,
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전결
|
||||
*/
|
||||
public function preDecide(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$approval = $this->service->preDecide($id, $request->get('comment'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '전결 처리되었습니다.',
|
||||
'data' => $approval,
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 복사 재기안
|
||||
*/
|
||||
public function copyForRedraft(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$approval = $this->service->copyForRedraft($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '문서가 복사되었습니다.',
|
||||
'data' => $approval,
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 열람 추적
|
||||
*/
|
||||
public function markAsRead(int $id): JsonResponse
|
||||
{
|
||||
$this->service->markAsRead($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '열람 처리되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 유틸
|
||||
// =========================================================================
|
||||
|
||||
@@ -40,6 +40,8 @@ class Approval extends Model
|
||||
'completed_at',
|
||||
'current_step',
|
||||
'attachments',
|
||||
'recall_reason',
|
||||
'parent_doc_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
@@ -65,12 +67,15 @@ class Approval extends Model
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
public const STATUS_ON_HOLD = 'on_hold';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_CANCELLED,
|
||||
self::STATUS_ON_HOLD,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -111,6 +116,16 @@ public function referenceSteps(): HasMany
|
||||
->orderBy('step_order');
|
||||
}
|
||||
|
||||
public function parentDocument(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_doc_id');
|
||||
}
|
||||
|
||||
public function childDocuments(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_doc_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
@@ -140,6 +155,11 @@ public function scopeRejected($query)
|
||||
return $query->where('status', self::STATUS_REJECTED);
|
||||
}
|
||||
|
||||
public function scopeOnHold($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_ON_HOLD);
|
||||
}
|
||||
|
||||
public function scopeCompleted($query)
|
||||
{
|
||||
return $query->whereIn('status', [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED]);
|
||||
@@ -169,11 +189,26 @@ public function isActionable(): bool
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function isCancellable(): bool
|
||||
public function isHoldable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function isHoldReleasable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ON_HOLD;
|
||||
}
|
||||
|
||||
public function isCancellable(): bool
|
||||
{
|
||||
return in_array($this->status, [self::STATUS_PENDING, self::STATUS_ON_HOLD]);
|
||||
}
|
||||
|
||||
public function isCopyable(): bool
|
||||
{
|
||||
return in_array($this->status, [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED]);
|
||||
}
|
||||
|
||||
public function isDeletable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
@@ -187,6 +222,7 @@ public function getStatusLabelAttribute(): string
|
||||
self::STATUS_APPROVED => '완료',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_CANCELLED => '회수',
|
||||
self::STATUS_ON_HOLD => '보류',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
@@ -199,6 +235,7 @@ public function getStatusColorAttribute(): string
|
||||
self::STATUS_APPROVED => 'green',
|
||||
self::STATUS_REJECTED => 'red',
|
||||
self::STATUS_CANCELLED => 'yellow',
|
||||
self::STATUS_ON_HOLD => 'amber',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
74
app/Models/Approvals/ApprovalDelegation.php
Normal file
74
app/Models/Approvals/ApprovalDelegation.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Approvals;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ApprovalDelegation extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_delegations';
|
||||
|
||||
protected $casts = [
|
||||
'form_ids' => 'array',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'notify_delegator' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'delegator_id',
|
||||
'delegate_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'form_ids',
|
||||
'notify_delegator',
|
||||
'is_active',
|
||||
'reason',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function delegator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'delegator_id');
|
||||
}
|
||||
|
||||
public function delegate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'delegate_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeForDelegator($query, int $userId)
|
||||
{
|
||||
return $query->where('delegator_id', $userId);
|
||||
}
|
||||
|
||||
public function scopeCurrentlyActive($query)
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
||||
return $query->active()
|
||||
->where('start_date', '<=', $today)
|
||||
->where('end_date', '>=', $today);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class ApprovalStep extends Model
|
||||
|
||||
protected $casts = [
|
||||
'step_order' => 'integer',
|
||||
'parallel_group' => 'integer',
|
||||
'acted_at' => 'datetime',
|
||||
'is_read' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
@@ -21,11 +22,14 @@ class ApprovalStep extends Model
|
||||
'approval_id',
|
||||
'step_order',
|
||||
'step_type',
|
||||
'parallel_group',
|
||||
'approver_id',
|
||||
'acted_by',
|
||||
'approver_name',
|
||||
'approver_department',
|
||||
'approver_position',
|
||||
'status',
|
||||
'approval_type',
|
||||
'comment',
|
||||
'acted_at',
|
||||
'is_read',
|
||||
@@ -49,11 +53,14 @@ class ApprovalStep extends Model
|
||||
|
||||
public const STATUS_SKIPPED = 'skipped';
|
||||
|
||||
public const STATUS_ON_HOLD = 'on_hold';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_SKIPPED,
|
||||
self::STATUS_ON_HOLD,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -70,6 +77,11 @@ public function approver(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'approver_id');
|
||||
}
|
||||
|
||||
public function actedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'acted_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
@@ -121,6 +133,7 @@ public function getStatusLabelAttribute(): string
|
||||
self::STATUS_APPROVED => '승인',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_SKIPPED => '건너뜀',
|
||||
self::STATUS_ON_HOLD => '보류',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,9 +82,12 @@ public function getCompletedByMe(int $userId, array $filters = [], int $perPage
|
||||
public function getReferencesForMe(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
$query = Approval::with(['form', 'drafter', 'steps.approver'])
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
->whereHas('steps', function ($q) use ($userId, $filters) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE);
|
||||
if (isset($filters['is_read'])) {
|
||||
$q->where('is_read', $filters['is_read'] === 'true' || $filters['is_read'] === '1');
|
||||
}
|
||||
});
|
||||
|
||||
$this->applyFilters($query, $filters);
|
||||
@@ -317,11 +320,11 @@ public function reject(int $id, string $comment): Approval
|
||||
}
|
||||
|
||||
/**
|
||||
* 회수 (기안자만, pending → cancelled)
|
||||
* 회수 (기안자만, pending/on_hold → cancelled)
|
||||
*/
|
||||
public function cancel(int $id): Approval
|
||||
public function cancel(int $id, ?string $recallReason = null): Approval
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
return DB::transaction(function () use ($id, $recallReason) {
|
||||
$approval = Approval::with('steps')->findOrFail($id);
|
||||
|
||||
if (! $approval->isCancellable()) {
|
||||
@@ -332,14 +335,140 @@ public function cancel(int $id): Approval
|
||||
throw new \InvalidArgumentException('기안자만 회수할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 모든 pending steps → skipped
|
||||
// 첫 번째 결재자가 이미 처리했으면 회수 불가
|
||||
$firstApproverStep = $approval->steps()
|
||||
->approvalOnly()
|
||||
->orderBy('step_order')
|
||||
->first();
|
||||
|
||||
if ($firstApproverStep && $firstApproverStep->status !== ApprovalStep::STATUS_PENDING
|
||||
&& $firstApproverStep->status !== ApprovalStep::STATUS_ON_HOLD) {
|
||||
throw new \InvalidArgumentException('첫 번째 결재자가 이미 처리하여 회수할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 모든 pending/on_hold steps → skipped
|
||||
$approval->steps()
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->whereIn('status', [ApprovalStep::STATUS_PENDING, ApprovalStep::STATUS_ON_HOLD])
|
||||
->update(['status' => ApprovalStep::STATUS_SKIPPED]);
|
||||
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_CANCELLED,
|
||||
'completed_at' => now(),
|
||||
'recall_reason' => $recallReason,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $approval->fresh(['form', 'drafter', 'steps.approver']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 (현재 결재자만, pending → on_hold)
|
||||
*/
|
||||
public function hold(int $id, string $comment): Approval
|
||||
{
|
||||
return DB::transaction(function () use ($id, $comment) {
|
||||
$approval = Approval::with('steps')->findOrFail($id);
|
||||
|
||||
if (! $approval->isHoldable()) {
|
||||
throw new \InvalidArgumentException('보류할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
$currentStep = $approval->getCurrentApproverStep();
|
||||
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
|
||||
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
|
||||
}
|
||||
|
||||
if (empty($comment)) {
|
||||
throw new \InvalidArgumentException('보류 사유를 입력해주세요.');
|
||||
}
|
||||
|
||||
$currentStep->update([
|
||||
'status' => ApprovalStep::STATUS_ON_HOLD,
|
||||
'comment' => $comment,
|
||||
'acted_at' => now(),
|
||||
]);
|
||||
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_ON_HOLD,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $approval->fresh(['form', 'drafter', 'steps.approver']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 해제 (보류한 결재자만, on_hold → pending)
|
||||
*/
|
||||
public function releaseHold(int $id): Approval
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$approval = Approval::with('steps')->findOrFail($id);
|
||||
|
||||
if (! $approval->isHoldReleasable()) {
|
||||
throw new \InvalidArgumentException('보류 해제할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
$holdStep = $approval->steps()
|
||||
->where('status', ApprovalStep::STATUS_ON_HOLD)
|
||||
->first();
|
||||
|
||||
if (! $holdStep || $holdStep->approver_id !== auth()->id()) {
|
||||
throw new \InvalidArgumentException('보류한 결재자만 해제할 수 있습니다.');
|
||||
}
|
||||
|
||||
$holdStep->update([
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
'comment' => null,
|
||||
'acted_at' => null,
|
||||
]);
|
||||
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_PENDING,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $approval->fresh(['form', 'drafter', 'steps.approver']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 전결 (현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인)
|
||||
*/
|
||||
public function preDecide(int $id, ?string $comment = null): Approval
|
||||
{
|
||||
return DB::transaction(function () use ($id, $comment) {
|
||||
$approval = Approval::with('steps')->findOrFail($id);
|
||||
|
||||
if (! $approval->isActionable()) {
|
||||
throw new \InvalidArgumentException('전결할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
$currentStep = $approval->getCurrentApproverStep();
|
||||
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
|
||||
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
|
||||
}
|
||||
|
||||
// 현재 step → approved + pre_decided
|
||||
$currentStep->update([
|
||||
'status' => ApprovalStep::STATUS_APPROVED,
|
||||
'approval_type' => 'pre_decided',
|
||||
'comment' => $comment,
|
||||
'acted_at' => now(),
|
||||
]);
|
||||
|
||||
// 이후 모든 pending approval/agreement steps → skipped
|
||||
$approval->steps()
|
||||
->where('step_order', '>', $currentStep->step_order)
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->where('status', ApprovalStep::STATUS_PENDING)
|
||||
->update(['status' => ApprovalStep::STATUS_SKIPPED]);
|
||||
|
||||
// 문서 최종 승인
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_APPROVED,
|
||||
'completed_at' => now(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
@@ -347,6 +476,78 @@ public function cancel(int $id): Approval
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 복사 재기안 (완료/반려/회수 문서를 복사하여 새 draft 생성)
|
||||
*/
|
||||
public function copyForRedraft(int $id): Approval
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$original = Approval::with('steps')->findOrFail($id);
|
||||
|
||||
if (! $original->isCopyable()) {
|
||||
throw new \InvalidArgumentException('복사할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
if ($original->drafter_id !== auth()->id()) {
|
||||
throw new \InvalidArgumentException('기안자만 복사할 수 있습니다.');
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 새 문서 생성
|
||||
$newApproval = Approval::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => $this->generateDocumentNumber($tenantId),
|
||||
'form_id' => $original->form_id,
|
||||
'line_id' => $original->line_id,
|
||||
'title' => $original->title,
|
||||
'content' => $original->content,
|
||||
'body' => $original->body,
|
||||
'status' => Approval::STATUS_DRAFT,
|
||||
'is_urgent' => $original->is_urgent,
|
||||
'drafter_id' => auth()->id(),
|
||||
'department_id' => $original->department_id,
|
||||
'current_step' => 0,
|
||||
'parent_doc_id' => $original->id,
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
// 결재선 복사 (모두 pending 상태로)
|
||||
foreach ($original->steps as $step) {
|
||||
ApprovalStep::create([
|
||||
'approval_id' => $newApproval->id,
|
||||
'step_order' => $step->step_order,
|
||||
'step_type' => $step->step_type,
|
||||
'approver_id' => $step->approver_id,
|
||||
'approver_name' => $step->approver_name,
|
||||
'approver_department' => $step->approver_department,
|
||||
'approver_position' => $step->approver_position,
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
]);
|
||||
}
|
||||
|
||||
return $newApproval->load(['form', 'drafter', 'steps.approver']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 열람 추적
|
||||
*/
|
||||
public function markAsRead(int $approvalId): void
|
||||
{
|
||||
$userId = auth()->id();
|
||||
|
||||
ApprovalStep::where('approval_id', $approvalId)
|
||||
->where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->where('is_read', false)
|
||||
->update([
|
||||
'is_read' => true,
|
||||
'read_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 결재선
|
||||
// =========================================================================
|
||||
@@ -428,9 +629,15 @@ public function getBadgeCounts(int $userId): array
|
||||
|
||||
$draftCount = Approval::draft()->byDrafter($userId)->count();
|
||||
|
||||
$referenceUnreadCount = ApprovalStep::where('approver_id', $userId)
|
||||
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
|
||||
->where('is_read', false)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'pending' => $pendingCount,
|
||||
'draft' => $draftCount,
|
||||
'reference_unread' => $referenceUnreadCount,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user