diff --git a/app/Http/Controllers/DailyLogController.php b/app/Http/Controllers/DailyLogController.php index 6fa56587..3a69e567 100644 --- a/app/Http/Controllers/DailyLogController.php +++ b/app/Http/Controllers/DailyLogController.php @@ -24,6 +24,7 @@ public function index(): View $projects = $this->projectService->getActiveProjects(); $stats = $this->dailyLogService->getStats($tenantId); $weeklyTimeline = $this->dailyLogService->getWeeklyTimeline($tenantId); + $pendingEntries = $this->dailyLogService->getPendingEntries($tenantId); $assigneeTypes = AdminPmDailyLogEntry::getAssigneeTypes(); $entryStatuses = AdminPmDailyLogEntry::getStatuses(); $assignees = $this->dailyLogService->getAssigneeList($tenantId); @@ -32,6 +33,7 @@ public function index(): View 'projects', 'stats', 'weeklyTimeline', + 'pendingEntries', 'assigneeTypes', 'entryStatuses', 'assignees' diff --git a/app/Services/ProjectManagement/DailyLogService.php b/app/Services/ProjectManagement/DailyLogService.php index c1c7453e..ed7d4205 100644 --- a/app/Services/ProjectManagement/DailyLogService.php +++ b/app/Services/ProjectManagement/DailyLogService.php @@ -441,4 +441,55 @@ public function getAssigneeList(int $tenantId): array 'teams' => $teams, ]; } + + /** + * 미완료 항목(예정, 진행중) 조회 - 담당자별 그룹핑, 날짜 오래된 순 정렬 + */ + public function getPendingEntries(int $tenantId, ?int $projectId = null, int $limit = 100): array + { + $query = AdminPmDailyLogEntry::query() + ->select('admin_pm_daily_log_entries.*') + ->with(['dailyLog:id,log_date,project_id', 'dailyLog.project:id,name']) + ->join('admin_pm_daily_logs', 'admin_pm_daily_log_entries.daily_log_id', '=', 'admin_pm_daily_logs.id') + ->where('admin_pm_daily_logs.tenant_id', $tenantId) + ->when($projectId, fn ($q) => $q->where('admin_pm_daily_logs.project_id', $projectId)) + ->whereIn('admin_pm_daily_log_entries.status', ['todo', 'in_progress']) + ->orderBy('admin_pm_daily_logs.log_date', 'asc') // 날짜 오래된 순 + ->orderBy('admin_pm_daily_log_entries.id', 'asc') + ->limit($limit); + + $entries = $query->get(); + + // 담당자별로 그룹핑 + $grouped = $entries->groupBy('assignee_name')->map(function ($items, $assigneeName) { + $todoItems = $items->where('status', 'todo'); + $inProgressItems = $items->where('status', 'in_progress'); + + // 항목들을 날짜 오래된 순으로 정렬 + $sortedEntries = $items->sortBy(fn ($e) => $e->dailyLog?->log_date)->values(); + + return [ + 'assignee_name' => $assigneeName, + 'total_count' => $items->count(), + 'todo_count' => $todoItems->count(), + 'in_progress_count' => $inProgressItems->count(), + 'oldest_date' => $sortedEntries->first()?->dailyLog?->log_date, // 카드 정렬용 + 'entries' => $sortedEntries->map(function ($entry) { + return [ + 'id' => $entry->id, + 'daily_log_id' => $entry->daily_log_id, + 'log_date' => $entry->dailyLog?->log_date, + 'project_name' => $entry->dailyLog?->project?->name, + 'content' => $entry->content, + 'status' => $entry->status, + ]; + })->values()->toArray(), + ]; + }); + + // 담당자 카드도 가장 오래된 항목 날짜 기준으로 정렬 + $sorted = $grouped->sortBy('oldest_date')->values()->toArray(); + + return $sorted; + } } diff --git a/resources/views/daily-logs/index.blade.php b/resources/views/daily-logs/index.blade.php index 41b9c6be..3a6050d9 100644 --- a/resources/views/daily-logs/index.blade.php +++ b/resources/views/daily-logs/index.blade.php @@ -25,7 +25,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition" @foreach($weeklyTimeline as $index => $day)
+ +@if(count($pendingEntries) > 0) +
+
+

+ + + + 미완료 항목 +

+ @php $totalPending = collect($pendingEntries)->sum('total_count'); @endphp + {{ count($pendingEntries) }}명 / {{ $totalPending }}건 +
+
+ @foreach($pendingEntries as $group) + @php + $entriesJson = collect($group['entries'])->map(fn($e) => [ + 'id' => $e['id'], + 'content' => $e['content'], + 'status' => $e['status'], + 'daily_log_id' => $e['daily_log_id'] ?? null, + ])->toJson(); + @endphp +
+ +
+
+ {{ $group['assignee_name'] }} + ({{ $group['total_count'] }}) +
+ +
+ +
+ @foreach($group['entries'] as $entry) +
+
+ + + {{ $entry['status'] === 'in_progress' ? '진행' : '예정' }} + + {{ \Carbon\Carbon::parse($entry['log_date'])->format('m/d') }} + + {{ $entry['content'] }} + +
+ @if($entry['status'] !== 'todo') + + @endif + @if($entry['status'] !== 'in_progress') + + @endif + +
+
+ + +
+ @endforeach +
+
+ @endforeach +
+
+@endif +
@@ -180,6 +266,65 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> 'entryStatuses' => $entryStatuses, 'assignees' => $assignees ]) + + +
@endsection @push('scripts') @@ -711,5 +856,334 @@ function openQuickAddTableEntry(logId) { tableOpenAccordionId = null; } }); + + // ======================================== + // 미완료 항목 펼치기/접기 + // ======================================== + function togglePendingEntry(textEl) { + const entryDiv = textEl.closest('[data-entry-id]'); + const fullDiv = entryDiv.querySelector('.pending-entry-full'); + + if (fullDiv.classList.contains('hidden')) { + // 펼치기 - 타이틀은 truncate 유지, 전체 내용 표시 + fullDiv.classList.remove('hidden'); + textEl.classList.add('text-blue-600'); + } else { + // 접기 + fullDiv.classList.add('hidden'); + textEl.classList.remove('text-blue-600'); + } + } + + // ======================================== + // 미완료 항목 상태 업데이트 + // ======================================== + function updatePendingStatus(entryId, status) { + fetch(`/api/admin/daily-logs/entries/${entryId}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ status }) + }) + .then(res => res.json()) + .then(result => { + if (result.success) { + const item = document.querySelector(`[data-entry-id="${entryId}"]`); + if (item) { + if (status === 'done') { + // 완료 처리: 항목 제거 애니메이션 + const parentCard = item.closest('.pending-assignee-card'); + item.style.transition = 'opacity 0.3s, transform 0.3s'; + item.style.opacity = '0'; + item.style.transform = 'scale(0.95)'; + setTimeout(() => { + item.remove(); + // 담당자 카드에 항목이 없으면 카드도 제거 + if (parentCard && parentCard.querySelectorAll('[data-entry-id]').length === 0) { + parentCard.style.transition = 'opacity 0.3s, transform 0.3s'; + parentCard.style.opacity = '0'; + parentCard.style.transform = 'scale(0.95)'; + setTimeout(() => { + parentCard.remove(); + // 전체 섹션 체크 + const grid = document.getElementById('pendingEntriesGrid'); + if (grid && grid.children.length === 0) { + grid.closest('.bg-white')?.remove(); + } + }, 300); + } + }, 300); + } else { + // 예정/진행중 변경: 상태 뱃지만 업데이트 + const badge = item.querySelector('span.px-1\\.5'); + if (badge) { + if (status === 'in_progress') { + badge.className = 'px-1.5 py-0.5 text-[10px] rounded shrink-0 bg-yellow-100 text-yellow-700'; + badge.textContent = '진행'; + } else { + badge.className = 'px-1.5 py-0.5 text-[10px] rounded shrink-0 bg-gray-100 text-gray-600'; + badge.textContent = '예정'; + } + } + // 상태변경 버튼들도 업데이트 + updatePendingEntryButtons(item, entryId, status); + } + } + // 로그 리스트도 새로고침 + htmx.trigger('#log-table', 'filterSubmit'); + } + }); + } + + // 미완료 항목 버튼 업데이트 + function updatePendingEntryButtons(item, entryId, currentStatus) { + const buttonsDiv = item.querySelector('.flex.gap-0\\.5'); + if (!buttonsDiv) return; + + let buttonsHtml = ''; + + if (currentStatus !== 'todo') { + buttonsHtml += ` + + `; + } + if (currentStatus !== 'in_progress') { + buttonsHtml += ` + + `; + } + buttonsHtml += ` + + `; + + buttonsDiv.innerHTML = buttonsHtml; + } + + // ======================================== + // 미완료 항목 수정 모달 관련 함수 + // ======================================== + let pendingEditEntries = []; + let pendingEditAssigneeName = ''; + + // 모달 열기 + function openPendingEditModal(entriesJson, assigneeName) { + pendingEditEntries = entriesJson; + pendingEditAssigneeName = assigneeName; + + document.getElementById('pendingEditAssigneeName').textContent = assigneeName; + + // 컨테이너 초기화 및 항목 추가 + const container = document.getElementById('pendingEditEntriesContainer'); + container.innerHTML = ''; + + entriesJson.forEach((entry, index) => { + addPendingEntryRow(entry, index); + }); + + document.getElementById('pendingEditModal').classList.remove('hidden'); + } + + // 모달 닫기 + function closePendingEditModal() { + document.getElementById('pendingEditModal').classList.add('hidden'); + pendingEditEntries = []; + pendingEditAssigneeName = ''; + } + + // 항목 행 추가 + function addPendingEntryRow(entry = null, index = null) { + const container = document.getElementById('pendingEditEntriesContainer'); + if (index === null) { + index = container.children.length; + } + + const currentStatus = entry?.status || 'todo'; + + const html = ` +
+ +
+ + + +
+ +
+ +
+ + + +
+ `; + + container.insertAdjacentHTML('beforeend', html); + } + + // 상태 버튼 토글 + function togglePendingStatus(btn, status) { + const row = btn.closest('.pending-edit-row'); + const statusInput = row.querySelector('input[name*="[status]"]'); + const buttons = row.querySelectorAll('.status-btn'); + + // 모든 버튼 스타일 초기화 + buttons.forEach(b => { + b.classList.remove('bg-gray-200', 'ring-2', 'ring-gray-400', 'bg-yellow-100', 'ring-yellow-400', 'bg-green-100', 'ring-green-400'); + }); + + // 선택된 버튼 스타일 적용 + if (status === 'todo') { + btn.classList.add('bg-gray-200', 'ring-2', 'ring-gray-400'); + } else if (status === 'in_progress') { + btn.classList.add('bg-yellow-100', 'ring-2', 'ring-yellow-400'); + } else if (status === 'done') { + btn.classList.add('bg-green-100', 'ring-2', 'ring-green-400'); + } + + // hidden input 값 업데이트 + statusInput.value = status; + } + + // 항목 행 삭제 + function removePendingEntryRow(btn) { + const row = btn.closest('.pending-edit-row'); + row.remove(); + reindexPendingEntryRows(); + } + + // 인덱스 재정렬 + function reindexPendingEntryRows() { + const rows = document.querySelectorAll('.pending-edit-row'); + rows.forEach((row, index) => { + row.dataset.index = index; + row.querySelectorAll('[name^="pending_entries["]').forEach(input => { + input.name = input.name.replace(/pending_entries\[\d+\]/, `pending_entries[${index}]`); + }); + }); + } + + // 폼 제출 + function submitPendingEditEntries(event) { + event.preventDefault(); + + const submitBtn = document.getElementById('pendingEditSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = '저장 중...'; + + const rows = document.querySelectorAll('.pending-edit-row'); + const updatePromises = []; + const deletePromises = []; + + // 기존 항목 ID 수집 + const currentIds = Array.from(rows).map(row => row.dataset.entryId).filter(id => id); + const originalIds = pendingEditEntries.map(e => String(e.id)); + + // 삭제할 항목 + const deletedIds = originalIds.filter(id => !currentIds.includes(id)); + deletedIds.forEach(id => { + deletePromises.push( + fetch(`/api/admin/daily-logs/entries/${id}`, { + method: 'DELETE', + headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + ); + }); + + // 수정/추가할 항목 + rows.forEach(row => { + const entryId = row.dataset.entryId; + const status = row.querySelector('input[name*="[status]"]').value; + const content = row.querySelector('textarea').value; + const dailyLogId = row.querySelector('input[name*="[daily_log_id]"]').value; + + if (!content.trim()) return; + + if (entryId) { + // 기존 항목 수정 + updatePromises.push( + fetch(`/api/admin/daily-logs/entries/${entryId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ content, status }) + }) + ); + } else if (dailyLogId) { + // 새 항목 추가 + updatePromises.push( + fetch(`/api/admin/daily-logs/${dailyLogId}/entries`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ + assignee_type: 'user', + assignee_name: pendingEditAssigneeName, + content, + status + }) + }) + ); + } + }); + + Promise.all([...deletePromises, ...updatePromises]) + .then(() => { + closePendingEditModal(); + // 페이지 새로고침으로 미완료 항목 갱신 + location.reload(); + }) + .catch(err => { + console.error(err); + alert('저장 중 오류가 발생했습니다.'); + }) + .finally(() => { + submitBtn.disabled = false; + submitBtn.textContent = '저장'; + }); + } + + // ESC 키로 모달 닫기 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + if (!document.getElementById('pendingEditModal').classList.contains('hidden')) { + closePendingEditModal(); + } + } + }); @endpush \ No newline at end of file diff --git a/resources/views/daily-logs/partials/table.blade.php b/resources/views/daily-logs/partials/table.blade.php index 48502be4..9091350a 100644 --- a/resources/views/daily-logs/partials/table.blade.php +++ b/resources/views/daily-logs/partials/table.blade.php @@ -1,125 +1,136 @@ - - - - - - - - - - - - - - - @forelse($logs as $log) - - - - - - - - - - - - - - - @empty - - - - @endforelse - -
날짜프로젝트요약항목작성자상태액션
-
- - - -
-
- {{ $log->log_date->format('Y-m-d') }} -
-
- {{ $log->log_date->format('l') }} + +
+ @forelse($logs as $log) + +
+ +
+
+ +
+ +
+
+ + + +
+
+ {{ $log->log_date->format('m/d') }} +
+
+ {{ $log->log_date->format('D') }} +
+
-
-
- @if($log->project) - - {{ $log->project->name }} - - @else - 전체 - @endif - - @if($log->summary) -
- {{ Str::limit($log->summary, 50) }} -
- @else - - - @endif -
-
- {{ $log->entries_count }}개 - @if($log->entries->count() > 0) -
- @php - $stats = $log->entry_stats; - @endphp - @if($stats['todo'] > 0) - - @endif - @if($stats['in_progress'] > 0) - - @endif - @if($stats['done'] > 0) - + + +
+
+ @if($log->project) + + {{ $log->project->name }} + + @endif + @if($log->trashed()) + + 삭제됨 + + @endif +
+ @if($log->summary) +

+ {!! nl2br(e($log->summary)) !!} +

+ @else +

요약 없음

@endif
- @endif
-
- {{ $log->creator?->name ?? '-' }} - - @if($log->trashed()) - - 삭제됨 - - @else - - 활성 - - @endif - - @if($log->trashed()) - - - @if(auth()->user()?->is_super_admin) - - @endif - @else - - - - @endif -
- 일일 로그가 없습니다. -
+
+ + + + + + @empty +
+ 일일 로그가 없습니다. +
+ @endforelse + @if($logs->hasPages()) @@ -127,3 +138,254 @@ class="text-red-600 hover:text-red-900">삭제 {{ $logs->withQueryString()->links() }} @endif + + \ No newline at end of file diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index c8941ee4..43186ce6 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -20,6 +20,48 @@ class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-700 hover: + +
  • + + +
  • +
  • - -
  • - - -
  • -
  • - - - + +
    + @foreach($groupedEntries->sortBy(fn($e) => $e->status === 'done' ? 1 : 0) as $entry) +
    +
    + + @if($entry->status === 'done') + 완료 + @elseif($entry->status === 'in_progress') + 진행 + @else + 예정 + @endif + + {{ $entry->content }} + +
    + @if($entry->status !== 'todo') + + @endif + @if($entry->status !== 'in_progress') + + @endif + @if($entry->status !== 'done') + + @endif
    - @endforeach
    - @empty -

    -

    - @endforelse -
    - - - -
    -
    - - 진행중 - ({{ $inProgressEntries->count() }}) -
    -
    - @forelse($inProgressEntries->groupBy('assignee_name') as $assigneeName => $groupedEntries) - @php - $entriesJson = $groupedEntries->map(fn($e) => [ - 'id' => $e->id, - 'daily_log_id' => $e->daily_log_id, - 'content' => $e->content, - 'status' => $e->status - ])->values()->toJson(); - @endphp -
    -

    {{ $assigneeName }}

    -
    - @foreach($groupedEntries as $entry) -
    - {{ Str::limit($entry->content, 25) }} -
    - - - -
    -
    - @endforeach -
    -
    - @empty -

    -

    - @endforelse -
    -
    - - -
    -
    - - - - 완료 - ({{ $doneEntries->count() }}) -
    -
    - @forelse($doneEntries->groupBy('assignee_name') as $assigneeName => $groupedEntries) - @php - $entriesJson = $groupedEntries->map(fn($e) => [ - 'id' => $e->id, - 'daily_log_id' => $e->daily_log_id, - 'content' => $e->content, - 'status' => $e->status - ])->values()->toJson(); - @endphp -
    -

    {{ $assigneeName }}

    -
    - @foreach($groupedEntries as $entry) -
    - {{ Str::limit($entry->content, 25) }} -
    - - - -
    -
    - @endforeach -
    -
    - @empty -

    -

    - @endforelse + @endforeach
    + @endforeach @else @@ -381,7 +321,7 @@ class="text-xs text-gray-500 hover:text-gray-700"> @endif @@ -986,4 +926,4 @@ function closeEditEntryModal() { } } -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/project-management/projects/index.blade.php b/resources/views/project-management/projects/index.blade.php index f4efd3f5..e2e1bb25 100644 --- a/resources/views/project-management/projects/index.blade.php +++ b/resources/views/project-management/projects/index.blade.php @@ -6,12 +6,7 @@
    - - - - - 대시보드 - +

    @@ -20,6 +15,12 @@