main
vue 130 lines 4.35 KB
Raw
1 <template>
2 <n-spin :show="loading">
3 <div class="flex flex-col gap-3">
4 <div class="flex items-center gap-3 text-sm">
5 <Chip :value="tasks.length" label="tasks" :bordered="false" />
6 <Chip v-if="totalDone > 0" :value="totalDone" label="done" type="success" :bordered="false" />
7 <Chip
8 v-if="mandatoryIncomplete > 0"
9 :bordered="false"
10 :value="mandatoryIncomplete"
11 label="mandatory incomplete"
12 type="warning"
13 />
14 </div>
15
16 <p class="text-xs">Read-only view of investigation tasks performed by the SOC team on this case.</p>
17
18 <div v-if="tasks.length" class="flex flex-col gap-3">
19 <CardEntity
20 v-for="task in tasks"
21 :key="task.id"
22 :status="
23 task.status === 'DONE' ? 'success' : task.status === 'NOT_NECESSARY' ? 'warning' : undefined
24 "
25 embedded
26 >
27 <template #header-main>
28 <div class="flex flex-wrap items-center gap-3">
29 <span class="text-default font-sans text-base">
30 {{ task.title }}
31 </span>
32
33 <n-tag v-if="task.mandatory" :bordered="false" type="error" size="small">mandatory</n-tag>
34 <n-tag v-if="task.template_task_id == null" :bordered="false" type="default" size="small">
35 custom
36 </n-tag>
37 </div>
38 </template>
39 <template #header-extra>
40 <Chip :value="statusLabel(task.status)" :type="statusTagType(task.status)" :bordered="false" />
41 </template>
42 <template #default>
43 <div class="flex flex-col gap-3">
44 <p v-if="task.description" class="text-secondary text-sm">{{ task.description }}</p>
45
46 <details v-if="task.guidelines" class="text-sm">
47 <summary class="cursor-pointer font-medium">Guidelines</summary>
48 <p class="text-secondary mt-1 whitespace-pre-line">{{ task.guidelines }}</p>
49 </details>
50 </div>
51 </template>
52 <template v-if="task.evidence_comment" #main-extra>
53 <div class="flex flex-col gap-1">
54 <div class="text-secondary text-xs uppercase">Notes from analyst</div>
55 <p class="text-sm whitespace-pre-line">
56 {{ task.evidence_comment }}
57 </p>
58 </div>
59 </template>
60 <template #footer>
61 <div class="flex flex-wrap items-center justify-between gap-2">
62 <div class="text-secondary flex flex-wrap gap-x-4 gap-y-1 text-sm">
63 <span v-if="task.completed_by">
64 {{ task.status === "DONE" ? "Completed" : "Marked" }} by
65 <strong>{{ task.completed_by }}</strong>
66 <template v-if="task.completed_at">
67 · {{ formatDate(task.completed_at, dFormats.datetime) }}
68 </template>
69 </span>
70 <span v-else>
71 Created by
72 <strong>{{ task.created_by }}</strong>
73 </span>
74 </div>
75
76 <div></div>
77 </div>
78 </template>
79 </CardEntity>
80 </div>
81 <n-empty v-else-if="!loading" description="No tasks on this case yet" class="h-32 justify-center" />
82 </div>
83 </n-spin>
84 </template>
85
86 <script setup lang="ts">
87 import type { CaseTask, CaseTaskStatus } from "@/types/caseTemplates"
88 import type { ApiError } from "@/types/common"
89 import { NEmpty, NSpin, NTag, useMessage } from "naive-ui"
90 import { computed, ref, watch } from "vue"
91 import Api from "@/api"
92 import CardEntity from "@/components/common/cards/CardEntity.vue"
93 import Chip from "@/components/common/Chip.vue"
94 import { useSettingsStore } from "@/stores/settings"
95 import { getApiErrorMessage } from "@/utils"
96 import { formatDate } from "@/utils/format"
97
98 const props = defineProps<{
99 caseId: number
100 }>()
101
102 const message = useMessage()
103 const tasks = ref<CaseTask[]>([])
104 const loading = ref(false)
105 const dFormats = useSettingsStore().dateFormat
106 const totalDone = computed(() => tasks.value.filter(t => t.status === "DONE").length)
107 const mandatoryIncomplete = computed(() => tasks.value.filter(t => t.mandatory && t.status !== "DONE").length)
108
109 function statusLabel(s: CaseTaskStatus): string {
110 return s === "TODO" ? "To do" : s === "DONE" ? "Done" : "Not necessary"
111 }
112
113 function statusTagType(s: CaseTaskStatus) {
114 return s === "DONE" ? "success" : s === "NOT_NECESSARY" ? "warning" : "default"
115 }
116
117 async function fetchTasks() {
118 loading.value = true
119 try {
120 const res = await Api.caseTemplates.getCaseTasks(props.caseId)
121 tasks.value = res.data.tasks ?? []
122 } catch (err) {
123 message.error(getApiErrorMessage(err as ApiError))
124 } finally {
125 loading.value = false
126 }
127 }
128
129 watch(() => props.caseId, fetchTasks, { immediate: true })
130 </script>