main
vue 436 lines 12.1 KB
Raw
1 <template>
2 <div>
3 <CardEntity
4 :loading="loadingDetails || loadingDelete"
5 :loading-description="loadingDelete ? 'Deleting Soc Case' : 'Loading Soc Case'"
6 :embedded
7 hoverable
8 clickable
9 @click.stop="showDetails = true"
10 >
11 <template #headerMain>{{ baseInfo?.case_uuid }}</template>
12 <template v-if="caseOpenDate" #headerExtra>
13 <n-popover overlap placement="top-end">
14 <template #trigger>
15 <div class="hover:text-primary flex cursor-help items-center gap-2">
16 <span>
17 {{ formatDate(caseOpenDate) }}
18 </span>
19 <Icon :name="TimeIcon" :size="16" />
20 </div>
21 </template>
22 <div class="flex flex-col px-1 py-2">
23 <n-timeline>
24 <n-timeline-item
25 type="success"
26 :title="`Open ${openedBy ? `[${openedBy}]` : ''}`"
27 :time="formatDate(caseOpenDate)"
28 />
29 <n-timeline-item
30 v-if="caseCloseDate"
31 title="Close date"
32 :time="formatDate(caseCloseDate)"
33 />
34 </n-timeline>
35 </div>
36 </n-popover>
37 </template>
38 <template #default>
39 <div class="flex flex-col gap-1">
40 <div v-html="baseInfo?.case_name"></div>
41 <p v-if="baseInfo?.case_description">
42 {{ excerpt }}
43 </p>
44 </div>
45 </template>
46 <template v-if="baseInfo" #mainExtra>
47 <div class="flex flex-wrap items-center gap-3">
48 <Badge type="splitted" :color="baseInfo.state_name === StateName.Open ? 'warning' : 'primary'">
49 <template #iconLeft>
50 <Icon :name="StatusIcon" :size="14" />
51 </template>
52 <template #label>State</template>
53 <template #value>
54 {{ baseInfo.state_name }}
55 </template>
56 </Badge>
57 <Badge type="splitted" color="primary">
58 <template #iconLeft>
59 <Icon :name="OwnerIcon" :size="16" />
60 </template>
61 <template #label>Owner</template>
62 <template #value>
63 {{ baseInfo.owner }}
64 </template>
65 </Badge>
66 <Badge type="splitted" color="primary">
67 <template #iconLeft>
68 <Icon :name="CustomerIcon" :size="13" />
69 </template>
70 <template #label>Client</template>
71 <template #value>
72 {{ clientName || "-" }}
73 </template>
74 </Badge>
75 <Badge
76 v-if="baseInfo.case_soc_id && !hideSocAlertLink"
77 type="active"
78 class="cursor-pointer"
79 @click.stop="openSocAlert()"
80 >
81 <template #iconRight>
82 <Icon :name="LinkIcon" :size="14" />
83 </template>
84 <template #label>Alert #{{ baseInfo.case_soc_id }}</template>
85 </Badge>
86 </div>
87 </template>
88 <template #footerExtra>
89 <SocCaseItemActions
90 v-if="!hideSocCaseAction"
91 class="flex flex-col justify-center gap-2"
92 :case-data="baseInfo"
93 size="small"
94 @closed="setClosed()"
95 @reopened="setReopened()"
96 @deleted="deleteCase()"
97 @start-deleting="loadingDelete = true"
98 />
99 </template>
100 </CardEntity>
101
102 <n-modal
103 v-model:show="showSocAlertDetails"
104 preset="card"
105 content-class="p-0!"
106 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
107 :title="`SOC Alert: #${baseInfo?.case_soc_id}`"
108 :bordered="false"
109 segmented
110 >
111 <div class="flex h-full w-full items-center justify-center">
112 <SocAlertItem
113 v-if="baseInfo?.case_soc_id"
114 :alert-id="baseInfo.case_soc_id"
115 embedded
116 hide-bookmark-action
117 hide-soc-case-action
118 class="w-full"
119 />
120 </div>
121 </n-modal>
122
123 <n-modal
124 v-model:show="showDetails"
125 preset="card"
126 content-class="p-0!"
127 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
128 :title="`SOC Case: ${baseInfo?.case_uuid}`"
129 :bordered="false"
130 segmented
131 >
132 <n-tabs type="line" animated :tabs-padding="24">
133 <n-tab-pane name="Info" tab="Info" display-directive="show">
134 <n-spin :show="loadingDetails">
135 <div v-if="extendedInfo" class="px-7 py-4">
136 <div v-if="tags.length" class="mb-2 flex gap-2">
137 <code v-for="tag of tags" :key="tag">{{ tag }}</code>
138 </div>
139 <div>{{ extendedInfo.case_name }}</div>
140 </div>
141 <div v-if="extendedInfo" class="flex flex-col gap-2 px-7 py-4">
142 <div v-if="baseInfo && !hideSocAlertLink" class="box">
143 soc id:
144 <code class="text-primary cursor-pointer" @click.stop="openSocAlert()">
145 #{{ baseInfo.case_soc_id }}
146 <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
147 </code>
148 </div>
149 <div v-if="extendedInfo?.protagonists && extendedInfo?.protagonists.length" class="box">
150 protagonists:
151 <code v-for="protagonist of extendedInfo.protagonists" :key="protagonist" class="mr-2">
152 {{ protagonist }}
153 </code>
154 </div>
155 </div>
156 <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
157 <CardKV v-for="(value, key) of properties" :key>
158 <template #key>
159 {{ key }}
160 </template>
161 <template #value>
162 <template v-if="key === 'customer_code' && value && value !== 'Customer Not Found'">
163 <code
164 class="text-primary cursor-pointer"
165 @click.stop="routeCustomer({ code: value }).navigate()"
166 >
167 #{{ value }}
168 <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
169 </code>
170 </template>
171 <template v-else>
172 {{ value || "-" }}
173 </template>
174 </template>
175 </CardKV>
176 </div>
177 </n-spin>
178 </n-tab-pane>
179 <n-tab-pane name="Description" tab="Description" display-directive="show">
180 <div v-if="baseInfo" class="p-6 pt-3">
181 <n-input
182 :value="baseInfo.case_description"
183 type="textarea"
184 readonly
185 placeholder="Empty"
186 size="large"
187 :autosize="{
188 minRows: 3
189 }"
190 />
191 </div>
192 </n-tab-pane>
193 <n-tab-pane name="History" tab="History" display-directive="show:lazy">
194 <n-spin :show="loadingDetails">
195 <div class="p-6 pt-3">
196 <SocCaseTimeline v-if="extendedInfo" :case-data="extendedInfo" />
197 </div>
198 </n-spin>
199 </n-tab-pane>
200 <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
201 <SocCaseAssetsList v-if="baseInfo" :case-id="baseInfo.case_id" />
202 </n-tab-pane>
203 <n-tab-pane name="Notes" tab="Notes" display-directive="show:lazy">
204 <div class="px-4">
205 <n-collapse v-model:expanded-names="noteFormVisible" display-directive="show">
206 <template #arrow>
207 <div class="mx-4 flex">
208 <Icon :name="AddIcon" />
209 </div>
210 </template>
211 <n-collapse-item name="1">
212 <template #header>
213 <div class="-ml-2 py-3">New note</div>
214 </template>
215 <div class="-mt-2 p-3 pt-0">
216 <SocCaseNoteForm
217 v-if="baseInfo"
218 :case-id="baseInfo.case_id"
219 @close="noteFormVisible = []"
220 @added="updateNotes = true"
221 />
222 </div>
223 </n-collapse-item>
224 </n-collapse>
225 </div>
226 <n-divider class="my-2!" />
227 <SocCaseNotesList v-if="baseInfo" v-model:requested="updateNotes" :case-id="baseInfo.case_id" />
228 </n-tab-pane>
229 </n-tabs>
230 </n-modal>
231 </div>
232 </template>
233
234 <script setup lang="ts">
235 import type { SocCase, SocCaseExt } from "@/types/soc/case.d"
236 import _omit from "lodash/omit"
237 import _split from "lodash/split"
238 import {
239 NCollapse,
240 NCollapseItem,
241 NDivider,
242 NInput,
243 NModal,
244 NPopover,
245 NSpin,
246 NTabPane,
247 NTabs,
248 NTimeline,
249 NTimelineItem,
250 useMessage
251 } from "naive-ui"
252 import { computed, defineAsyncComponent, onBeforeMount, ref, watch } from "vue"
253 import Api from "@/api"
254 import Badge from "@/components/common/Badge.vue"
255 import CardEntity from "@/components/common/cards/CardEntity.vue"
256 import CardKV from "@/components/common/cards/CardKV.vue"
257 import Icon from "@/components/common/Icon.vue"
258 import { useNavigation } from "@/composables/useNavigation"
259 import { useSettingsStore } from "@/stores/settings"
260 import { StateName } from "@/types/soc/case.d"
261 import dayjs from "@/utils/dayjs"
262
263 const { caseData, caseId, embedded, hideSocCaseAction, hideSocAlertLink } = defineProps<{
264 caseData?: SocCase
265 caseId?: number | string
266 embedded?: boolean
267 hideSocAlertLink?: boolean
268 hideSocCaseAction?: boolean
269 }>()
270 const emit = defineEmits<{
271 (e: "deleted"): void
272 }>()
273 const SocCaseTimeline = defineAsyncComponent(() => import("./SocCaseTimeline.vue"))
274 const SocCaseAssetsList = defineAsyncComponent(() => import("./SocCaseAssetsList.vue"))
275 const SocCaseNoteForm = defineAsyncComponent(() => import("./SocCaseNoteForm.vue"))
276 const SocCaseNotesList = defineAsyncComponent(() => import("./SocCaseNotesList.vue"))
277 const SocCaseItemActions = defineAsyncComponent(() => import("./SocCaseItemActions.vue"))
278 const SocAlertItem = defineAsyncComponent(() => import("../SocAlerts/SocAlertItem/SocAlertItem.vue"))
279
280 const TimeIcon = "carbon:time"
281 const CustomerIcon = "carbon:user"
282 const LinkIcon = "carbon:launch"
283 const OwnerIcon = "carbon:user-military"
284 const StatusIcon = "fluent:status-20-regular"
285 const AddIcon = "carbon:add-alt"
286
287 const { routeCustomer } = useNavigation()
288 const showSocAlertDetails = ref(false)
289 const showDetails = ref(false)
290 const loadingDetails = ref(false)
291 const loadingDelete = ref(false)
292 const message = useMessage()
293 const noteFormVisible = ref([])
294 const updateNotes = ref(false)
295
296 const extendedInfo = ref<SocCaseExt | null>(null)
297 const baseInfo = computed<(SocCase & Partial<SocCaseExt>) | null>(() => {
298 if (caseData) {
299 return { ...caseData, ...(extendedInfo.value || {}) }
300 }
301 return null
302 })
303
304 const dFormats = useSettingsStore().dateFormat
305
306 const caseOpenDate = computed<string | null>(() => {
307 if (baseInfo.value?.case_open_date) {
308 return baseInfo.value.case_open_date as string
309 }
310 if (baseInfo.value?.open_date) {
311 return baseInfo.value.open_date as string
312 }
313 if (extendedInfo.value?.open_date) {
314 return extendedInfo.value.open_date as string
315 }
316 return null
317 })
318
319 const caseCloseDate = computed<string | null>(() => {
320 if (baseInfo.value?.case_close_date) {
321 return baseInfo.value.case_close_date as string
322 }
323 if (baseInfo.value?.close_date) {
324 return baseInfo.value.close_date as string
325 }
326 if (extendedInfo.value?.close_date) {
327 return extendedInfo.value.close_date as string
328 }
329 return null
330 })
331
332 const clientName = computed<string | null>(() => {
333 if (baseInfo.value?.client_name) {
334 return baseInfo.value.client_name as string
335 }
336 return null
337 })
338
339 const openedBy = computed<string | null>(() => {
340 if (baseInfo.value?.opened_by) {
341 return baseInfo.value.opened_by as string
342 }
343 return null
344 })
345
346 const excerpt = computed(() => {
347 const text = caseData?.case_description || ""
348 const truncated = text.split(" ").slice(0, 30).join(" ")
349
350 return truncated + (truncated !== text ? "..." : "")
351 })
352
353 const tags = computed<string[]>(() => {
354 if (!extendedInfo?.value?.case_tags) {
355 return []
356 }
357
358 return _split(extendedInfo.value?.case_tags, ",")
359 })
360
361 const properties = computed(() => {
362 return _omit(extendedInfo.value, [
363 "case_description",
364 "case_name",
365 "case_soc_id",
366 "case_tags",
367 "case_uuid",
368 "close_date",
369 "initial_date",
370 "modification_history",
371 "open_by_user",
372 "open_by_user_id",
373 "open_date",
374 "protagonists"
375 ])
376 })
377
378 function formatDate(timestamp: string | number | Date, utc: boolean = true): string {
379 return dayjs(timestamp).utc(utc).format(dFormats.date)
380 }
381
382 function setClosed() {
383 if (baseInfo.value) {
384 baseInfo.value.state_name = StateName.Closed
385 }
386 }
387
388 function setReopened() {
389 if (baseInfo.value) {
390 baseInfo.value.state_name = StateName.Open
391 }
392 }
393
394 function deleteCase() {
395 loadingDelete.value = false
396 emit("deleted")
397 }
398
399 function openSocAlert() {
400 showSocAlertDetails.value = true
401 }
402
403 function getDetails() {
404 if (caseId || baseInfo.value) {
405 loadingDetails.value = true
406
407 Api.soc
408 .getCases(caseId?.toString() || baseInfo.value?.case_id.toString())
409 .then(res => {
410 if (res.data.success) {
411 extendedInfo.value = res.data?.case || null
412 } else {
413 message.warning(res.data?.message || "An error occurred. Please try again later.")
414 }
415 })
416 .catch(err => {
417 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
418 })
419 .finally(() => {
420 loadingDetails.value = false
421 })
422 }
423 }
424
425 watch(showDetails, val => {
426 if (val && !extendedInfo.value) {
427 getDetails()
428 }
429 })
430
431 onBeforeMount(() => {
432 if (!caseData && caseId) {
433 getDetails()
434 }
435 })
436 </script>