main
vue 370 lines 9.39 KB
Raw
1 <template>
2 <n-spin :show="loading" class="customer-provisioning-default-settings-form">
3 <n-form ref="formRef" :label-width="80" :model="form" :rules>
4 <div class="flex flex-col gap-3">
5 <div>
6 <n-form-item label="Name" path="case_name">
7 <n-input v-model:value.trim="form.case_name" placeholder="Input the case name..." clearable />
8 </n-form-item>
9 </div>
10 <div>
11 <n-form-item label="Description" path="case_description">
12 <n-input
13 v-model:value.trim="form.case_description"
14 placeholder="Input the case description..."
15 clearable
16 type="textarea"
17 :autosize="{
18 minRows: 5,
19 maxRows: 15
20 }"
21 />
22 </n-form-item>
23 </div>
24 <div class="flex gap-3">
25 <n-form-item label="Status" path="case_status" class="basis-1/2">
26 <n-select
27 v-model:value="form.case_status"
28 :options="statusOptions"
29 placeholder="Value..."
30 clearable
31 to="body"
32 />
33 </n-form-item>
34 <n-form-item label="Assigned user" path="assigned_to" class="basis-1/2">
35 <n-select
36 v-model:value="form.assigned_to"
37 :options="usersOptions"
38 placeholder="Value..."
39 clearable
40 to="body"
41 />
42 </n-form-item>
43 </div>
44 <div>
45 <n-form-item label="Customer" path="customer_code">
46 <n-select
47 v-model:value="form.customer_code"
48 :options="customersOptions"
49 placeholder="Value..."
50 clearable
51 to="body"
52 />
53 </n-form-item>
54 </div>
55 <div>
56 <n-form-item label="Template (optional)" path="template_id">
57 <n-select
58 v-model:value="selectedTemplateId"
59 :options="templateOptions"
60 :loading="loadingTemplates"
61 placeholder="Apply a template on creation (optional)"
62 clearable
63 filterable
64 to="body"
65 :render-label="renderOption"
66 size="large"
67 />
68 </n-form-item>
69 <p class="text-secondary -mt-2 text-xs">
70 Applies the template's predefined tasks to the case. Filtered by the selected customer; global
71 templates are always shown.
72 </p>
73 </div>
74
75 <div class="flex justify-between gap-4">
76 <div class="flex gap-4">
77 <slot name="additionalActions"></slot>
78 </div>
79 <div class="flex gap-4">
80 <n-button :disabled="loading" @click="reset()">Reset</n-button>
81 <n-button type="primary" :disabled="!isValid" :loading="submitting" @click="validate()">
82 Submit
83 </n-button>
84 </div>
85 </div>
86 </div>
87 </n-form>
88 </n-spin>
89 </template>
90
91 <script setup lang="ts">
92 import type { FormInst, FormRules, FormValidationError } from "naive-ui"
93 import type { Ref } from "vue"
94 import type { Customer } from "@/types/customers.d"
95 import type { Case, CasePayload, CaseStatus } from "@/types/incidentManagement/cases.d"
96 import type { CaseTemplate } from "@/types/incidentManagement/caseTemplates.d"
97 import _get from "lodash/get"
98 import _trim from "lodash/trim"
99 import { NButton, NForm, NFormItem, NInput, NSelect, NSpin, useMessage } from "naive-ui"
100 import { computed, h, inject, onBeforeMount, onMounted, ref, watch } from "vue"
101 import Api from "@/api"
102
103 const emit = defineEmits<{
104 (e: "update:loading", value: boolean): void
105 (e: "submitted", value: Case): void
106 (
107 e: "mounted",
108 value: {
109 load: () => void
110 }
111 ): void
112 }>()
113
114 const loadingAvailableUsers = ref(false)
115 const loadingCustomersList = ref(false)
116 const loadingTemplates = ref(false)
117 const submitting = ref(false)
118 const loading = computed(
119 () => loadingAvailableUsers.value || loadingCustomersList.value || loadingTemplates.value || submitting.value
120 )
121 // Template picker state. Lives outside form because it's a query param to the
122 // API call, not a body field on CasePayload.
123 const availableTemplates = ref<CaseTemplate[]>([])
124 const selectedTemplateId = ref<number | null>(null)
125 const templateOptions = computed(() =>
126 availableTemplates.value.map(t => ({
127 label: t.name,
128 name: t.name,
129 customer_code: t.customer_code,
130 source: t.source,
131 is_default: t.is_default,
132 value: t.id
133 }))
134 )
135 const message = useMessage()
136 const form = ref<CasePayload>(getForm())
137 const formRef = ref<FormInst | null>(null)
138 const availableUsers = inject<Ref<string[]>>("assignable-users", ref([]))
139 const customersList = inject<Ref<Customer[]>>("customers-list", ref([]))
140
141 const rules: FormRules = {
142 case_name: {
143 message: "Please input the Name",
144 required: true,
145 trigger: ["input", "blur"]
146 },
147 case_description: {
148 message: "Please input the Description",
149 required: true,
150 trigger: ["input", "blur"]
151 },
152 assigned_to: {
153 message: "Please input the Assigned user",
154 required: true,
155 trigger: ["input", "blur"]
156 },
157 case_status: {
158 message: "Please input the Status",
159 required: true,
160 trigger: ["input", "blur"]
161 },
162 customer_code: {
163 message: "Please input the Customer",
164 required: true,
165 trigger: ["input", "blur"]
166 }
167 }
168
169 const statusOptions: { label: string; value: CaseStatus }[] = [
170 { label: "Open", value: "OPEN" },
171 { label: "Closed", value: "CLOSED" },
172 { label: "In progress", value: "IN_PROGRESS" }
173 ]
174
175 const usersOptions = computed(() => availableUsers.value.map(o => ({ label: o, value: o })))
176 const customersOptions = computed(() =>
177 customersList.value.map(o => ({ label: `#${o.customer_code} - ${o.customer_name}`, value: o.customer_code }))
178 )
179
180 const isValid = computed(() => {
181 let valid = true
182
183 for (const key in rules) {
184 const rule = rules[key] as FormRules
185
186 if (rule.required && !_trim(_get(form.value, key))) {
187 valid = false
188 }
189 }
190
191 return valid
192 })
193
194 function validate() {
195 if (!formRef.value) return
196
197 formRef.value.validate((errors?: Array<FormValidationError>) => {
198 if (!errors) {
199 submit()
200 } else {
201 message.warning("You must fill in the required fields correctly.")
202 return false
203 }
204 })
205 }
206
207 function getForm() {
208 const payload = {
209 case_name: "",
210 case_creation_time: new Date(),
211 case_description: "",
212 assigned_to: null,
213 case_status: null,
214 customer_code: null,
215 comments: []
216 }
217 return payload
218 }
219
220 function reset(force?: boolean) {
221 if (!loading.value || force) {
222 resetForm()
223 formRef.value?.restoreValidation()
224 }
225 }
226
227 function resetForm() {
228 form.value = getForm()
229 }
230
231 function renderOption(option: {
232 label: string
233 value: number
234 name?: string | null
235 is_default?: boolean
236 customer_code?: string | null
237 source?: string | null
238 }) {
239 const title = `${option.name}${option.is_default ? " (default)" : ""}`
240 const description = [option.customer_code, option.source].filter(Boolean).join("")
241 return h("div", { class: "flex flex-col gap-0.5 leading-none py-2" }, [
242 h("span", title),
243 h("span", { class: "text-secondary text-xs" }, description)
244 ])
245 }
246
247 function submit() {
248 submitting.value = true
249
250 const params: Record<string, number> = {}
251 if (selectedTemplateId.value != null) {
252 params.template_id = selectedTemplateId.value
253 }
254
255 Api.incidentManagement.cases
256 .createCase(form.value, params)
257 .then(res => {
258 if (res.data.success) {
259 message.success(res.data?.message || "Case created successfully")
260 reset(true)
261 emit("submitted", res.data.case)
262 } else {
263 message.warning(res.data?.message || "An error occurred. Please try again later.")
264 }
265 })
266 .catch(err => {
267 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
268 })
269 .finally(() => {
270 submitting.value = false
271 })
272 }
273
274 function getTemplates(customerCode?: string | null) {
275 loadingTemplates.value = true
276 Api.incidentManagement.caseTemplates
277 .listTemplates({
278 customerCode: customerCode || undefined,
279 includeGlobal: true
280 })
281 .then(res => {
282 if (res.data.success) {
283 availableTemplates.value = res.data.templates
284 }
285 })
286 .catch(err => {
287 // Soft failure — template picker is optional. Log via console rather
288 // than spamming a toast, since the user can still submit without one.
289 console.warn("Failed to load templates for picker:", err)
290 })
291 .finally(() => {
292 loadingTemplates.value = false
293 })
294 }
295
296 function getAvailableUsers() {
297 loadingAvailableUsers.value = true
298
299 Api.incidentManagement.alerts
300 .getAvailableUsers()
301 .then(res => {
302 if (res.data.success) {
303 availableUsers.value = res.data?.available_users || []
304 } else {
305 message.warning(res.data?.message || "An error occurred. Please try again later.")
306 }
307 })
308 .catch(err => {
309 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
310 })
311 .finally(() => {
312 loadingAvailableUsers.value = false
313 })
314 }
315
316 function getCustomers() {
317 loadingCustomersList.value = true
318
319 Api.customers
320 .getCustomers()
321 .then(res => {
322 if (res.data.success) {
323 customersList.value = res.data?.customers || []
324 } else {
325 message.warning(res.data?.message || "An error occurred. Please try again later.")
326 }
327 })
328 .catch(err => {
329 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
330 })
331 .finally(() => {
332 loadingCustomersList.value = false
333 })
334 }
335
336 function load() {
337 if (!availableUsers.value.length) {
338 getAvailableUsers()
339 }
340 if (!customersList.value.length) {
341 getCustomers()
342 }
343 getTemplates(form.value.customer_code)
344 reset()
345 }
346
347 // Re-fetch templates when the customer changes so the picker only offers
348 // applicable + global templates. Reset the selection if it no longer applies.
349 watch(
350 () => form.value.customer_code,
351 newCustomer => {
352 selectedTemplateId.value = null
353 getTemplates(newCustomer)
354 }
355 )
356
357 watch(loading, val => {
358 emit("update:loading", val)
359 })
360
361 onBeforeMount(() => {
362 load()
363 })
364
365 onMounted(() => {
366 emit("mounted", {
367 load
368 })
369 })
370 </script>