main
vue 154 lines 4.47 KB
Raw
1 <template>
2 <n-popselect
3 v-model:value="statusSelected"
4 v-model:show="listVisible"
5 :options="statusOptions"
6 :loading
7 size="medium"
8 scrollable
9 to="body"
10 >
11 <slot :loading />
12 </n-popselect>
13
14 <!-- Soft-warning modal (issue #792 Phase 3 backend / Phase 5 UI). Fires
15 when closing a case with mandatory tasks not marked DONE. Cancel
16 reverts the dropdown; "Close anyway" re-submits with force=true. -->
17 <n-modal
18 v-model:show="showWarning"
19 preset="card"
20 title="Mandatory tasks incomplete"
21 style="max-width: 560px"
22 display-directive="show"
23 >
24 <p>
25 This case has {{ pendingTasks.length }} mandatory task{{ pendingTasks.length === 1 ? "" : "s" }} that
26 {{ pendingTasks.length === 1 ? "is" : "are" }} not marked
27 <strong>Done</strong>
28 . Closing anyway will record the override in the case timeline.
29 </p>
30
31 <ul class="text-sm">
32 <li v-for="t in pendingTasks" :key="t.id" class="mb-1">
33 <span class="font-medium">{{ t.title }}</span>
34 <span class="text-tertiary">{{ humanStatus(t.status) }}</span>
35 </li>
36 </ul>
37
38 <template #footer>
39 <div class="flex justify-end gap-2">
40 <n-button @click="cancelClose">Cancel</n-button>
41 <n-button type="warning" :loading @click="confirmForceClose">Close anyway</n-button>
42 </div>
43 </template>
44 </n-modal>
45 </template>
46
47 <script setup lang="ts">
48 import type { ApiError } from "@/types/common"
49 import type { Case, CaseStatus } from "@/types/incidentManagement/cases.d"
50 import type { CaseTask, CaseTaskStatus } from "@/types/incidentManagement/caseTemplates.d"
51 import { NButton, NModal, NPopselect, useMessage } from "naive-ui"
52 import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
53 import Api from "@/api"
54 import { getApiErrorMessage } from "@/utils"
55
56 const props = defineProps<{
57 caseData: Case
58 }>()
59 const emit = defineEmits<{
60 (e: "updated", value: Case): void
61 }>()
62
63 const { caseData } = toRefs(props)
64
65 const loading = ref(false)
66 const message = useMessage()
67 const listVisible = ref(false)
68 const status = computed(() => caseData.value.case_status)
69 const statusOptions = ref<{ label: string; value: CaseStatus }[]>([
70 { label: "Open", value: "OPEN" },
71 { label: "In progress", value: "IN_PROGRESS" },
72 { label: "Closed", value: "CLOSED" }
73 ])
74 const statusSelected = ref<CaseStatus | null>(null)
75
76 // Soft-warning state
77 const showWarning = ref(false)
78 const pendingTasks = ref<CaseTask[]>([])
79 // Snapshot the status the dropdown was switching to so we can resubmit with
80 // force=true after the user confirms. Separate from statusSelected because
81 // canceling needs to revert the dropdown without retriggering this watcher.
82 const pendingTargetStatus = ref<CaseStatus | null>(null)
83
84 function humanStatus(s: CaseTaskStatus): string {
85 return s === "TODO" ? "to do" : s === "DONE" ? "done" : "not necessary"
86 }
87
88 async function callUpdate(target: CaseStatus, force = false) {
89 loading.value = true
90 try {
91 const res = await Api.incidentManagement.cases.updateCaseStatus(caseData.value.id, target, force)
92 const data: any = res.data
93
94 // Soft-warning shape from backend: success=false, requires_confirmation=true,
95 // incomplete_mandatory_tasks=[]. Treat as a confirmation flow rather than an error.
96 if (data && data.requires_confirmation === true) {
97 pendingTasks.value = data.incomplete_mandatory_tasks ?? []
98 pendingTargetStatus.value = target
99 showWarning.value = true
100 return
101 }
102
103 if (data?.success) {
104 emit("updated", { ...caseData.value, case_status: target })
105 } else {
106 message.warning(data?.message || "An error occurred. Please try again later.")
107 // Revert dropdown on plain failure.
108 revertDropdown()
109 }
110 } catch (err) {
111 message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.")
112 revertDropdown()
113 } finally {
114 loading.value = false
115 }
116 }
117
118 function revertDropdown() {
119 if (status.value && statusSelected.value !== status.value) {
120 statusSelected.value = status.value
121 }
122 }
123
124 function cancelClose() {
125 showWarning.value = false
126 pendingTargetStatus.value = null
127 pendingTasks.value = []
128 revertDropdown()
129 }
130
131 async function confirmForceClose() {
132 if (pendingTargetStatus.value == null) {
133 showWarning.value = false
134 return
135 }
136 const target = pendingTargetStatus.value
137 showWarning.value = false
138 await callUpdate(target, true)
139 pendingTargetStatus.value = null
140 pendingTasks.value = []
141 }
142
143 watch(statusSelected, newVal => {
144 if (newVal && newVal !== status.value) {
145 callUpdate(newVal, false)
146 }
147 })
148
149 onBeforeMount(() => {
150 if (status.value) {
151 statusSelected.value = status.value
152 }
153 })
154 </script>