main
vue 348 lines 9.5 KB
Raw
1 <template>
2 <div>
3 <CardEntity
4 :loading
5 :embedded
6 hoverable
7 :size="compact ? 'small' : 'medium'"
8 :clickable="compact"
9 :highlighted="highlight"
10 >
11 <template v-if="caseEntity" #headerMain>
12 <div
13 class="flex items-center gap-2 wrap-break-word"
14 :class="{ 'hover:text-primary cursor-pointer': !compact }"
15 @click="compact ? undefined : openDetails()"
16 >
17 <span>#{{ caseEntity.id }}</span>
18 <Icon v-if="!compact" :name="InfoIcon" :size="16" />
19 </div>
20 </template>
21 <template v-if="caseEntity?.case_creation_time" #headerExtra>
22 {{ formatDate(caseEntity.case_creation_time, dFormats.datetime) }}
23 </template>
24
25 <template v-if="caseEntity" #default>
26 {{ caseEntity.case_name }}
27 </template>
28
29 <template v-if="caseEntity" #mainExtra>
30 <div v-if="compact" class="flex flex-wrap items-center gap-3">
31 <Badge
32 type="splitted"
33 class="cursor-pointer"
34 bright
35 :color="
36 caseEntity.case_status === 'OPEN'
37 ? 'danger'
38 : caseEntity.case_status === 'IN_PROGRESS'
39 ? 'warning'
40 : caseEntity.case_status === 'CLOSED'
41 ? 'success'
42 : undefined
43 "
44 >
45 <template #iconLeft>
46 <StatusIcon :status="caseEntity.case_status" />
47 </template>
48 <template #label>Status</template>
49 <template #value>
50 <div class="flex items-center gap-2">
51 {{ caseEntity.case_status || "n/d" }}
52 </div>
53 </template>
54 </Badge>
55
56 <Badge
57 type="splitted"
58 class="cursor-pointer"
59 bright
60 :color="caseEntity.assigned_to ? 'success' : undefined"
61 >
62 <template #iconLeft>
63 <AssigneeIcon :assignee="caseEntity.assigned_to" />
64 </template>
65 <template #label>Assignee</template>
66 <template #value>
67 <div class="flex items-center gap-2">
68 {{ caseEntity.assigned_to || "n/d" }}
69 </div>
70 </template>
71 </Badge>
72 </div>
73
74 <div v-else class="flex flex-wrap items-center gap-3">
75 <CaseStatusSwitch
76 v-slot="{ loading: loadingStatus }"
77 :case-data="caseEntity"
78 @updated="updateCase($event)"
79 >
80 <Badge
81 type="splitted"
82 class="cursor-pointer"
83 bright
84 :color="
85 caseEntity.case_status === 'OPEN'
86 ? 'danger'
87 : caseEntity.case_status === 'IN_PROGRESS'
88 ? 'warning'
89 : caseEntity.case_status === 'CLOSED'
90 ? 'success'
91 : undefined
92 "
93 >
94 <template #iconLeft>
95 <n-spin :size="12" :show="loadingStatus" content-class="flex flex-col justify-center">
96 <StatusIcon :status="caseEntity.case_status" />
97 </n-spin>
98 </template>
99 <template #label>Status</template>
100 <template #value>
101 <div class="flex items-center gap-2">
102 {{ caseEntity.case_status || "n/d" }}
103 <Icon :name="EditIcon" :size="13" />
104 </div>
105 </template>
106 </Badge>
107 </CaseStatusSwitch>
108
109 <CaseAssignUser
110 v-slot="{ loading: loadingAssignee }"
111 :case-data="caseEntity"
112 @updated="updateCase($event)"
113 >
114 <Badge
115 type="splitted"
116 class="cursor-pointer"
117 bright
118 :color="caseEntity.assigned_to ? 'success' : undefined"
119 >
120 <template #iconLeft>
121 <n-spin :size="12" :show="loadingAssignee" content-class="flex flex-col justify-center">
122 <AssigneeIcon :assignee="caseEntity.assigned_to" />
123 </n-spin>
124 </template>
125 <template #label>Assignee</template>
126 <template #value>
127 <div class="flex items-center gap-2">
128 {{ caseEntity.assigned_to || "n/d" }}
129 <Icon :name="EditIcon" :size="13" />
130 </div>
131 </template>
132 </Badge>
133 </CaseAssignUser>
134
135 <Badge v-if="caseEntity.customer_code" type="splitted" class="hidden! sm:flex!">
136 <template #label>Customer</template>
137 <template #value>
138 <div class="flex h-full items-center">
139 <code
140 class="text-primary cursor-pointer leading-none"
141 @click.stop="routeCustomer({ code: caseEntity.customer_code }).navigate()"
142 >
143 #{{ caseEntity.customer_code }}
144 <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
145 </code>
146 </div>
147 </template>
148 </Badge>
149 </div>
150 </template>
151
152 <template v-if="caseEntity && !compact" #footer>
153 <div class="flex items-center justify-between">
154 <n-collapse :trigger-areas="['main', 'arrow']">
155 <n-collapse-item name="alerts-list">
156 <template #header>
157 <span class="whitespace-nowrap">
158 Alerts
159 <code class="ml-2">{{ caseEntity.alerts.length }}</code>
160 </span>
161 </template>
162 <template #header-extra>
163 <div class="actions-box ml-2 flex flex-wrap items-center justify-end gap-2">
164 <n-button quaternary size="tiny" class="xs:flex! hidden!" @click="handleDelete()">
165 Delete Case
166 </n-button>
167 <CaseReportButton :case-id="caseEntity.id" size="tiny" />
168 <CaseNotificationButton
169 :case-id="caseEntity.id"
170 :notification-invoked-number="caseEntity.notification_invoked_number || 0"
171 size="tiny"
172 @invoked="getCase(caseEntity.id)"
173 />
174 </div>
175 </template>
176 <div class="flex flex-col gap-2">
177 <template v-if="caseEntity.alerts.length">
178 <AlertItem
179 v-for="alert of caseEntity.alerts"
180 :key="alert.id"
181 :alert-data="alert"
182 compact
183 @deleted="getCase(caseEntity.id)"
184 @updated="getCase(caseEntity.id)"
185 />
186 </template>
187 <template v-else>
188 <n-empty
189 v-if="!loading"
190 description="No alerts attached"
191 class="h-24 justify-center"
192 />
193 </template>
194 </div>
195 </n-collapse-item>
196 </n-collapse>
197 </div>
198 </template>
199 </CardEntity>
200
201 <n-modal
202 v-model:show="showDetails"
203 :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(482px, 90vh)', overflow: 'hidden' }"
204 >
205 <n-card
206 content-class="flex flex-col p-0!"
207 :title="caseNameTruncated"
208 closable
209 :bordered="false"
210 segmented
211 role="modal"
212 @close="closeDetails()"
213 >
214 <CaseDetails
215 v-if="caseEntity"
216 :case-data="caseEntity"
217 @deleted="emitDelete()"
218 @updated="updateCase($event)"
219 />
220 </n-card>
221 </n-modal>
222 </div>
223 </template>
224
225 <script setup lang="ts">
226 import type { Case } from "@/types/incidentManagement/cases.d"
227 import _clone from "lodash/cloneDeep"
228 import _truncate from "lodash/truncate"
229 import { NButton, NCard, NCollapse, NCollapseItem, NEmpty, NModal, NSpin, useDialog, useMessage } from "naive-ui"
230 import { computed, onBeforeMount, onMounted, ref, toRefs } from "vue"
231 import Api from "@/api"
232 import Badge from "@/components/common/Badge.vue"
233 import CardEntity from "@/components/common/cards/CardEntity.vue"
234 import Icon from "@/components/common/Icon.vue"
235 import { useNavigation } from "@/composables/useNavigation"
236 import { useSettingsStore } from "@/stores/settings"
237 import { formatDate } from "@/utils/format"
238 import AlertItem from "../alerts/AlertItem.vue"
239 import AssigneeIcon from "../common/AssigneeIcon.vue"
240 import StatusIcon from "../common/StatusIcon.vue"
241 import CaseAssignUser from "./CaseAssignUser.vue"
242 import CaseDetails from "./CaseDetails.vue"
243 import CaseNotificationButton from "./CaseNotificationButton.vue"
244 import CaseReportButton from "./CaseReportButton.vue"
245 import CaseStatusSwitch from "./CaseStatusSwitch.vue"
246 import { handleDeleteCase } from "./utils"
247
248 const props = defineProps<{
249 caseData?: Case
250 caseId?: number
251 compact?: boolean
252 embedded?: boolean
253 detailsOnMounted?: boolean
254 highlight?: boolean
255 }>()
256 const emit = defineEmits<{
257 (e: "opened"): void
258 (e: "deleted"): void
259 (e: "updated", value: Case): void
260 }>()
261
262 const { caseData, caseId, compact, embedded, detailsOnMounted, highlight } = toRefs(props)
263
264 const LinkIcon = "carbon:launch"
265 const InfoIcon = "carbon:information"
266 const EditIcon = "uil:edit-alt"
267
268 const { routeCustomer } = useNavigation()
269 const dialog = useDialog()
270 const message = useMessage()
271 const dFormats = useSettingsStore().dateFormat
272 const loading = ref(false)
273 const showDetails = ref(false)
274 const caseEntity = ref<Case | null>(null)
275 const caseNameTruncated = computed(() => _truncate(caseEntity.value?.case_name, { length: 50 }))
276
277 function updateCase(updatedCase: Case) {
278 caseEntity.value = updatedCase
279 emit("updated", updatedCase)
280 }
281
282 function getCase(caseId: number) {
283 loading.value = true
284
285 Api.incidentManagement.cases
286 .getCase(caseId)
287 .then(res => {
288 if (res.data.success) {
289 caseEntity.value = res.data?.cases?.[0] || null
290 } else {
291 message.warning(res.data?.message || "An error occurred. Please try again later.")
292 }
293 })
294 .catch(err => {
295 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
296 })
297 .finally(() => {
298 loading.value = false
299 })
300 }
301
302 function handleDelete() {
303 if (caseEntity.value) {
304 handleDeleteCase({
305 caseData: caseEntity.value,
306 cbBefore: () => {
307 loading.value = true
308 },
309 cbSuccess: () => {
310 emitDelete()
311 },
312 cbAfter: () => {
313 loading.value = false
314 },
315 message,
316 dialog
317 })
318 }
319 }
320
321 function emitDelete() {
322 showDetails.value = false
323 emit("deleted")
324 }
325
326 function openDetails() {
327 emit("opened")
328 showDetails.value = true
329 }
330
331 function closeDetails() {
332 showDetails.value = false
333 }
334
335 onBeforeMount(() => {
336 if (caseId.value) {
337 getCase(caseId.value)
338 } else if (caseData.value) {
339 caseEntity.value = _clone(caseData.value)
340 }
341 })
342
343 onMounted(() => {
344 if (detailsOnMounted.value) {
345 openDetails()
346 }
347 })
348 </script>