main
vue 197 lines 5.17 KB
Raw
1 <template>
2 <n-spin :show="loadingDetails">
3 <div class="@container min-h-50">
4 <n-alert v-if="detailsError" title="Error" type="error" :description="detailsError" />
5
6 <n-tabs v-else-if="caseData" type="line" animated>
7 <n-tab-pane name="overview" tab="Overview">
8 <div class="flex flex-col gap-4">
9 <CaseOverview
10 :case-data
11 @status-updated="handleStatusUpdated"
12 @assigned-to-updated="handleAssignedToUpdated"
13 />
14 <div class="flex justify-end">
15 <n-popconfirm to="body" @positive-click="handleDeleteCase">
16 <template #trigger>
17 <n-button
18 size="small"
19 :focusable="false"
20 :loading="deleting"
21 type="error"
22 secondary
23 >
24 <template #icon>
25 <Icon name="carbon:trash-can" />
26 </template>
27 Delete Case
28 </n-button>
29 </template>
30 Are you sure you want to delete this case?
31 </n-popconfirm>
32 </div>
33 </div>
34 </n-tab-pane>
35 <n-tab-pane
36 name="alerts"
37 :tab="`Alerts (${caseData?.alerts?.length || caseData?.alert_ids?.length || 0})`"
38 >
39 <CaseAlerts
40 :case-data
41 @updated="handleAlertUpdated"
42 @unlinked="handleAlertUnlinked"
43 @linked="handleAlertLinked"
44 />
45 </n-tab-pane>
46 <n-tab-pane name="tasks" tab="Tasks">
47 <CaseTasks :case-id="caseData.id" />
48 </n-tab-pane>
49 <n-tab-pane name="timeline" tab="Timeline">
50 <CaseTimeline :case-id="caseData.id" />
51 </n-tab-pane>
52 <n-tab-pane name="files" tab="Files">
53 <CaseFiles :case-id="caseData.id" />
54 </n-tab-pane>
55 <n-tab-pane name="comments" :tab="`Comments (${caseData.comments?.length || 0})`">
56 <CaseComments
57 :case-data
58 @added="handleCommentAdded"
59 @updated="handleCommentUpdated"
60 @deleted="handleCommentDeleted"
61 />
62 </n-tab-pane>
63 </n-tabs>
64 </div>
65 </n-spin>
66 </template>
67
68 <script setup lang="ts">
69 import type { CaseAssignedUpdateSuccessPayload } from "../CaseAssignedSelect.vue"
70 import type { CaseStatusUpdateSuccessPayload } from "../CaseStatusSelect.vue"
71 import type { Case } from "@/types/cases"
72 import type { CommentItem } from "@/types/comments"
73 import type { ApiError } from "@/types/common"
74 import { NAlert, NButton, NPopconfirm, NSpin, NTabPane, NTabs, useMessage } from "naive-ui"
75 import { ref, watch } from "vue"
76 import Api from "@/api"
77 import Icon from "@/components/common/Icon.vue"
78 import { getApiErrorMessage } from "@/utils"
79 import CaseAlerts from "./CaseAlerts.vue"
80 import CaseComments from "./CaseComments.vue"
81 import CaseFiles from "./CaseFiles.vue"
82 import CaseOverview from "./CaseOverview.vue"
83 import CaseTasks from "./CaseTasks.vue"
84 import CaseTimeline from "./CaseTimeline.vue"
85
86 const props = defineProps<{
87 caseId: number | null
88 }>()
89
90 const emit = defineEmits<{
91 (e: "statusUpdated", value: CaseStatusUpdateSuccessPayload): void
92 (e: "assignedToUpdated", value: CaseAssignedUpdateSuccessPayload): void
93 (e: "deleted"): void
94 }>()
95
96 const message = useMessage()
97 const caseData = ref<Case | null>(null)
98 const detailsError = ref<string | null>(null)
99 const loadingDetails = ref(false)
100 const deleting = ref(false)
101
102 async function loadCaseDetails() {
103 if (props.caseId === null) return
104
105 loadingDetails.value = true
106 detailsError.value = null
107
108 try {
109 const response = await Api.cases.getCase(props.caseId)
110 caseData.value = response.data.cases[0] || null
111 if (!caseData.value) {
112 detailsError.value = "Case not found."
113 }
114 } catch (err) {
115 detailsError.value = getApiErrorMessage(err as ApiError)
116 } finally {
117 loadingDetails.value = false
118 }
119 }
120
121 function handleCommentAdded(comment: CommentItem) {
122 if (!caseData.value) return
123
124 if (!caseData.value.comments) {
125 caseData.value.comments = []
126 }
127 caseData.value.comments.push(comment)
128 }
129
130 function handleCommentUpdated(comment: CommentItem) {
131 if (!caseData.value) return
132 caseData.value.comments = caseData.value.comments.map(c => (c.id === comment.id ? comment : c))
133 }
134
135 function handleCommentDeleted(commentId: number) {
136 if (!caseData.value) return
137 caseData.value.comments = caseData.value.comments.filter(c => c.id !== commentId)
138 }
139
140 function handleStatusUpdated(payload: CaseStatusUpdateSuccessPayload) {
141 if (!caseData.value) return
142 caseData.value.case_status = payload.status
143 emit("statusUpdated", payload)
144 }
145
146 function handleAssignedToUpdated(payload: CaseAssignedUpdateSuccessPayload) {
147 if (!caseData.value) return
148 caseData.value.assigned_to = payload.assignedTo
149 emit("assignedToUpdated", payload)
150 }
151
152 function handleAlertUpdated() {
153 loadCaseDetails()
154 }
155
156 function handleAlertUnlinked() {
157 loadCaseDetails()
158 }
159
160 function handleAlertLinked() {
161 loadCaseDetails()
162 }
163
164 function handleDeleteCase() {
165 if (!caseData.value) return
166
167 deleting.value = true
168
169 Api.cases
170 .deleteCase(caseData.value.id)
171 .then(res => {
172 if (res.data.success) {
173 message.success("Case deleted successfully")
174 emit("deleted")
175 }
176 })
177 .catch(err => {
178 message.error(getApiErrorMessage(err as ApiError))
179 })
180 .finally(() => {
181 deleting.value = false
182 })
183 }
184
185 watch(
186 () => props.caseId,
187 async newCaseId => {
188 caseData.value = null
189 detailsError.value = null
190
191 if (newCaseId !== null) {
192 await loadCaseDetails()
193 }
194 },
195 { immediate: true }
196 )
197 </script>