feat: [approval] 완료함 미읽음 알림 뱃지 기능 추가
- approvals 테이블에 drafter_read_at 컬럼 추가 (API 마이그레이션) - 승인/반려/전결 완료 시 drafter_read_at = null 설정 - getBadgeCounts()에 completed_unread 카운트 추가 - 사이드메뉴 완료함에 미읽음 뱃지 표시 (주황색) - 완료함 페이지 진입 시 일괄 읽음 처리 - 상세 페이지 열람 시 개별 읽음 처리
This commit is contained in:
@@ -494,6 +494,34 @@ public function badgeCounts(): JsonResponse
|
||||
return response()->json(['success' => true, 'data' => $counts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료함 읽음 처리 (일괄)
|
||||
*/
|
||||
public function markCompletedAsRead(): JsonResponse
|
||||
{
|
||||
$count = $this->service->markCompletedAsRead(auth()->id());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $count > 0 ? "{$count}건 읽음 처리되었습니다." : '새로운 완료 건이 없습니다.',
|
||||
'data' => ['marked_count' => $count],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 문서 기안자 읽음 처리
|
||||
*/
|
||||
public function markReadSingle(int $id): JsonResponse
|
||||
{
|
||||
$approval = $this->service->getApproval($id);
|
||||
|
||||
if ($approval->drafter_id === auth()->id() && ! $approval->drafter_read_at) {
|
||||
$approval->update(['drafter_read_at' => now()]);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 첨부파일
|
||||
// =========================================================================
|
||||
|
||||
@@ -20,6 +20,7 @@ class Approval extends Model
|
||||
'attachments' => 'array',
|
||||
'drafted_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'drafter_read_at' => 'datetime',
|
||||
'current_step' => 'integer',
|
||||
'is_urgent' => 'boolean',
|
||||
];
|
||||
@@ -38,6 +39,7 @@ class Approval extends Model
|
||||
'department_id',
|
||||
'drafted_at',
|
||||
'completed_at',
|
||||
'drafter_read_at',
|
||||
'current_step',
|
||||
'attachments',
|
||||
'recall_reason',
|
||||
|
||||
@@ -78,6 +78,7 @@ public function boot(): void
|
||||
$menuBadges['byUrl']['/approval-mgmt/pending'] = ['count' => $counts['pending'], 'color' => '#ef4444'];
|
||||
$menuBadges['byUrl']['/approval-mgmt/drafts'] = ['count' => $counts['draft'], 'color' => '#3b82f6'];
|
||||
$menuBadges['byUrl']['/approval-mgmt/references'] = ['count' => $counts['reference_unread'], 'color' => '#10b981'];
|
||||
$menuBadges['byUrl']['/approval-mgmt/completed'] = ['count' => $counts['completed_unread'], 'color' => '#f59e0b'];
|
||||
} catch (\Throwable $e) {
|
||||
// 테이블 미존재 등 예외 무시
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ public function approve(int $id, ?string $comment = null): Approval
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_APPROVED,
|
||||
'completed_at' => now(),
|
||||
'drafter_read_at' => null,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
@@ -344,6 +345,7 @@ public function reject(int $id, string $comment): Approval
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_REJECTED,
|
||||
'completed_at' => now(),
|
||||
'drafter_read_at' => null,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
@@ -507,6 +509,7 @@ public function preDecide(int $id, ?string $comment = null): Approval
|
||||
$approval->update([
|
||||
'status' => Approval::STATUS_APPROVED,
|
||||
'completed_at' => now(),
|
||||
'drafter_read_at' => null,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
@@ -753,13 +756,30 @@ public function getBadgeCounts(int $userId): array
|
||||
->where('is_read', false)
|
||||
->count();
|
||||
|
||||
$completedUnreadCount = Approval::where('drafter_id', $userId)
|
||||
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
|
||||
->whereNull('drafter_read_at')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'pending' => $pendingCount,
|
||||
'draft' => $draftCount,
|
||||
'reference_unread' => $referenceUnreadCount,
|
||||
'completed_unread' => $completedUnreadCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료함 미읽음 일괄 읽음 처리
|
||||
*/
|
||||
public function markCompletedAsRead(int $userId): int
|
||||
{
|
||||
return Approval::where('drafter_id', $userId)
|
||||
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
|
||||
->whereNull('drafter_read_at')
|
||||
->update(['drafter_read_at' => now()]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
@@ -39,12 +39,20 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadCompleted();
|
||||
markCompletedAsRead();
|
||||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
loadCompleted();
|
||||
});
|
||||
});
|
||||
|
||||
function markCompletedAsRead() {
|
||||
fetch('/api/admin/approvals/mark-completed-read', {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function loadCompleted(page = 1) {
|
||||
const form = document.getElementById('filterForm');
|
||||
const params = new URLSearchParams(new FormData(form));
|
||||
|
||||
@@ -237,6 +237,14 @@ class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition te
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
{{-- 기안자가 완료 문서 열람 시 읽음 처리 --}}
|
||||
@if($approval->drafter_id === auth()->id() && in_array($approval->status, ['approved', 'rejected']) && !$approval->drafter_read_at)
|
||||
fetch('/api/admin/approvals/{{ $approval->id }}/mark-read-single', {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).catch(() => {});
|
||||
@endif
|
||||
|
||||
async function processApproval(action) {
|
||||
const comment = document.getElementById('approval-comment')?.value || '';
|
||||
|
||||
|
||||
@@ -960,6 +960,8 @@
|
||||
Route::get('/forms', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'forms'])->name('forms');
|
||||
Route::get('/expense-history', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'expenseHistory'])->name('expense-history');
|
||||
Route::get('/badge-counts', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'badgeCounts'])->name('badge-counts');
|
||||
Route::post('/mark-completed-read', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'markCompletedAsRead'])->name('mark-completed-read');
|
||||
Route::post('/{id}/mark-read-single', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'markReadSingle'])->name('mark-read-single');
|
||||
Route::post('/upload-file', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'uploadFile'])->name('upload-file');
|
||||
Route::delete('/files/{fileId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'deleteFile'])->name('delete-file');
|
||||
Route::get('/files/{fileId}/download', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'downloadFile'])->name('download-file');
|
||||
|
||||
Reference in New Issue
Block a user