| 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> |