diff --git a/frontend/src/components/StatusSelect.vue b/frontend/src/components/StatusSelect.vue new file mode 100644 index 0000000..99e7311 --- /dev/null +++ b/frontend/src/components/StatusSelect.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/frontend/src/pages/relay/TaskDetailPage.vue b/frontend/src/pages/relay/TaskDetailPage.vue index 59076c9..c46c3b5 100644 --- a/frontend/src/pages/relay/TaskDetailPage.vue +++ b/frontend/src/pages/relay/TaskDetailPage.vue @@ -29,7 +29,7 @@ import { useTaskStore } from '@/stores/task.store' import { useAuthStore } from '@/stores/auth.store' import { useDialog } from '@/composables/useDialog' import UserAvatar from '@/components/UserAvatar.vue' -import DropdownSelect from '@/components/DropdownSelect.vue' +import StatusSelect from '@/components/StatusSelect.vue' import { attachmentKindOf, fileBadge } from '@/shared/utils/format' const route = useRoute() @@ -302,15 +302,24 @@ const isAssignee = computed( /* ============================================================ * 중간 점검(체크포인트) — 작업자가 점검일 당일에 보고서로 전송, 지시자는 기록 열람 * ============================================================ */ -const WORK_STATUS_LABEL: Record = { - todo: '대기', - doing: '진행 중', - done: '완료', +const WORK_STATUS_ORDER: ChecklistWorkStatus[] = ['todo', 'doing', 'done'] +// 진행 상태 표시 메타(라벨/색/약색) +const WORK_STATUS_META: Record< + ChecklistWorkStatus, + { label: string; color: string; weak: string } +> = { + todo: { label: '대기', color: '#9298a3', weak: '#eef0f2' }, + doing: { label: '진행 중', color: '#2563eb', weak: '#e9f0fd' }, + done: { label: '완료', color: '#1f9d57', weak: '#e7f5ed' }, } -const WORK_STATUS_OPTIONS: ChecklistWorkStatus[] = ['todo', 'doing', 'done'] -// 보고서 작성 모달의 진행 상태 드롭다운 옵션 -const WORK_STATUS_DD: { value: ChecklistWorkStatus; label: string }[] = - WORK_STATUS_OPTIONS.map((s) => ({ value: s, label: WORK_STATUS_LABEL[s] })) +// 카테고리 색(탭/그룹 dot) +const CAT_COLOR: Record = { + 필수: '#2563eb', + 관리: '#1f9d57', + 보안: '#dc2626', + 부가: '#b7791f', +} +type CatTab = ChecklistCategory | 'all' // KST(UTC+9) 기준 'yyyy-MM-dd' — '당일' 판정 function kstDate(iso: string | Date): string { @@ -338,19 +347,19 @@ function checkpointState(cp: TaskCheckpointView): CheckpointState { return 'future' } -// --- 보고서 작성(모달, 점검일 당일·담당자만) — 카테고리 탭 방식 --- +// --- 보고서 작성(모달, 점검일 당일·담당자만) — 전체/카테고리 탭 --- const reportingCpId = ref(null) const reportItems = ref< { id: string; text: string; category: ChecklistCategory; workStatus: ChecklistWorkStatus }[] >([]) const reportNote = ref('') const reportSubmitting = ref(false) -const writeTab = ref('필수') +const writeTab = ref('all') function openReportForm(cp: TaskCheckpointView) { if (!task.value) return reportingCpId.value = cp.id - writeTab.value = '필수' + writeTab.value = 'all' // 현재(마지막 보고) 상태로 프리필 reportItems.value = task.value.checklist .filter((c) => c.id) @@ -362,13 +371,26 @@ function openReportForm(cp: TaskCheckpointView) { })) reportNote.value = '' } -// 작성 모달 탭별 항목 / 개수 -function writeItemsByCat(cat: ChecklistCategory) { - return reportItems.value.filter((it) => it.category === cat) -} +// 작성 모달 — 탭별 그룹/개수/요약 +const writeGroups = computed(() => { + if (writeTab.value === 'all') { + return CHECKLIST_CATEGORIES.map((c) => ({ + cat: c, + items: reportItems.value.filter((i) => i.category === c), + })).filter((g) => g.items.length) + } + const c = writeTab.value + return [{ cat: c, items: reportItems.value.filter((i) => i.category === c) }] +}) function writeCatCount(cat: ChecklistCategory): number { - return writeItemsByCat(cat).length + return reportItems.value.filter((i) => i.category === cat).length } +const writeSummary = computed(() => + WORK_STATUS_ORDER.map((k) => ({ + k, + n: reportItems.value.filter((i) => i.workStatus === k).length, + })), +) function cancelReportForm() { reportingCpId.value = null reportItems.value = [] @@ -393,22 +415,32 @@ async function submitReport() { } } -// --- 보고 기록 열람(모달) --- +// --- 보고 기록 열람(모달) — 전체/카테고리 탭 --- const openedReport = ref(null) -const reportTab = ref('필수') +const viewTab = ref('all') // 보고 완료 체크포인트 클릭 → 해당 체크포인트의 최신 보고를 모달로 연다 function openReportModal(cp: TaskCheckpointView) { const r = task.value?.checkpointReports.find((x) => x.checkpointId === cp.id) if (!r) return openedReport.value = r - reportTab.value = '필수' + viewTab.value = 'all' } -function reportItemsByCat(cat: ChecklistCategory) { - return openedReport.value?.items.filter((it) => it.category === cat) ?? [] -} -function reportCatCount(cat: ChecklistCategory): number { - return reportItemsByCat(cat).length +const viewItems = computed(() => openedReport.value?.items ?? []) +const viewGroups = computed(() => { + const items = viewItems.value + if (viewTab.value === 'all') { + return CHECKLIST_CATEGORIES.map((c) => ({ + cat: c, + items: items.filter((i) => i.category === c), + })).filter((g) => g.items.length) + } + const c = viewTab.value + return [{ cat: c, items: items.filter((i) => i.category === c) }] +}) +function viewCatCount(cat: ChecklistCategory): number { + return viewItems.value.filter((i) => i.category === cat).length } +const reporterInitial = computed(() => openedReport.value?.reporterName?.[0] ?? '?') // 담당자 액션 카드 테마(시안): changes→빨강 / review→앰버 / done→초록 / 그 외→accent const assigneeCardClass = computed(() => { switch (task.value?.status) { @@ -1537,7 +1569,7 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { - +