main
vue 370 lines 9.24 KB
Raw
1 <template>
2 <n-spin :show="loading" class="customer-form">
3 <n-form ref="formRef" :label-width="80" :model :rules>
4 <div class="flex flex-col gap-0">
5 <n-form-item label="Name" path="name">
6 <n-input v-model:value.trim="model.name" placeholder="Exclusion rule name" clearable />
7 </n-form-item>
8
9 <n-form-item label="Description" path="description">
10 <n-input
11 v-model:value.trim="model.description"
12 placeholder="Exclusion rule description"
13 clearable
14 />
15 </n-form-item>
16
17 <n-form-item label="Channel" path="channel">
18 <n-input v-model:value.trim="model.channel" placeholder="Exclusion rule channel" clearable />
19 </n-form-item>
20
21 <n-form-item label="Title" path="title">
22 <n-input v-model:value.trim="model.title" placeholder="Exclusion rule title" clearable />
23 </n-form-item>
24
25 <div class="mb-6 flex flex-col gap-2">
26 <n-form-item path="field_matches" required label="Field matches" :show-feedback="false">
27 <div class="flex w-full flex-col gap-4">
28 <div
29 v-for="(field, index) of model.field_matches"
30 :key="field.id"
31 class="border-default relative flex w-full flex-col gap-2 rounded-xl border p-2"
32 >
33 <n-input v-model:value.trim="field.key" placeholder="Field name" clearable />
34 <n-input
35 v-model:value.trim="field.value"
36 placeholder="Field match"
37 clearable
38 type="textarea"
39 :autosize="{ minRows: 3 }"
40 />
41 <div class="absolute -top-2.5 -right-2.5">
42 <n-button
43 v-if="model.field_matches.length > 1"
44 circle
45 secondary
46 size="tiny"
47 type="error"
48 @click="delField(index)"
49 >
50 <template #icon>
51 <Icon :name="DelIcon" />
52 </template>
53 </n-button>
54 </div>
55 </div>
56 </div>
57 </n-form-item>
58
59 <div class="flex justify-end">
60 <n-button @click="addField()">
61 <template #icon>
62 <Icon :name="AddIcon" />
63 </template>
64 Add field
65 </n-button>
66 </div>
67
68 <n-alert v-if="!areFieldsFilled" type="warning">
69 <span class="text-sm">Please fill in all fields</span>
70 </n-alert>
71 <n-alert v-if="!areFieldsUniques && model.field_matches.length > 1" type="warning">
72 <span class="text-sm">Attention, there are duplicate fields</span>
73 </n-alert>
74 </div>
75
76 <n-form-item label="Customer" path="customer_code">
77 <n-select
78 v-model:value="model.customer_code"
79 :options="customersOptions"
80 placeholder="Select Customer..."
81 to="body"
82 filterable
83 :loading="loadingCustomers || !customersOptions.length"
84 />
85 </n-form-item>
86
87 <n-form-item path="enabled" label="Status">
88 <n-checkbox v-model:checked="model.enabled" size="large">Enabled</n-checkbox>
89 </n-form-item>
90
91 <div class="flex justify-between gap-4">
92 <div class="flex gap-4">
93 <slot name="additionalActions"></slot>
94 </div>
95 <div class="flex gap-4">
96 <n-button :disabled="loading" @click="reset()">Reset</n-button>
97 <n-button type="primary" :disabled="!isValid" :loading @click="validate()">Submit</n-button>
98 </div>
99 </div>
100 </div>
101 </n-form>
102 </n-spin>
103 </template>
104
105 <script setup lang="ts">
106 import type { FormInst, FormItemRule, FormRules, FormValidationError } from "naive-ui"
107 import type { ExclusionRulePayload } from "@/api/endpoints/incidentManagement/exclusionRules"
108 import type { Customer } from "@/types/customers.d"
109 import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules"
110 import _get from "lodash/get"
111 import _trim from "lodash/trim"
112 import { NAlert, NButton, NCheckbox, NForm, NFormItem, NInput, NSelect, NSpin, useMessage } from "naive-ui"
113 import { computed, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
114 import Api from "@/api"
115 import Icon from "@/components/common/Icon.vue"
116
117 interface FieldMatch {
118 id: string
119 key: string | null
120 value: string | null
121 }
122
123 interface Model extends Omit<ExclusionRulePayload, "field_matches"> {
124 field_matches: FieldMatch[]
125 }
126
127 const props = defineProps<{
128 entity?: ExclusionRule
129 resetOnSubmit?: boolean
130 }>()
131
132 const emit = defineEmits<{
133 (e: "update:loading", value: boolean): void
134 (e: "submitted", value: ExclusionRule): void
135 (
136 e: "mounted",
137 value: {
138 reset: () => void
139 }
140 ): void
141 }>()
142
143 const { entity, resetOnSubmit } = toRefs(props)
144
145 const DelIcon = "carbon:close-filled"
146 const AddIcon = "carbon:add"
147 const loading = ref(false)
148 const loadingCustomers = ref(false)
149 const message = useMessage()
150 const model = ref<Model>(getDefaultModel())
151 const formRef = ref<FormInst | null>(null)
152 const customersList = ref<Customer[]>([])
153
154 const customersOptions = computed(() =>
155 customersList.value.map(o => ({ label: `#${o.customer_code} - ${o.customer_name}`, value: o.customer_code }))
156 )
157
158 const areFieldsPresent = computed(() => {
159 return !!model.value.field_matches.length
160 })
161
162 const areFieldsFilled = computed(() => {
163 const fieldsFilled = model.value.field_matches.filter(o => (!!o.key && !o.value) || (!o.key && !!o.value))
164
165 return fieldsFilled.length === 0
166 })
167
168 const areFieldsUniques = computed(() => {
169 const fieldsFilled = model.value.field_matches.filter(o => !!o.key).map(o => o.key)
170
171 const uniques: (string | null)[] = fieldsFilled.filter((value, index, self) => self.indexOf(value) === index)
172
173 return uniques.length === fieldsFilled.length
174 })
175
176 const rules: FormRules = {
177 name: {
178 required: true,
179 message: "Please input name",
180 trigger: ["input", "blur"]
181 },
182 description: {
183 required: true,
184 message: "Please input description",
185 trigger: ["input", "blur"]
186 },
187 channel: {
188 required: true,
189 message: "Please input channel",
190 trigger: ["input", "blur"]
191 },
192 title: {
193 required: true,
194 message: "Please input title",
195 trigger: ["input", "blur"]
196 },
197 field_matches: {
198 required: false,
199
200 validator(_rule: FormItemRule, _value: string) {
201 if (!areFieldsPresent.value) {
202 return new Error(`Please fill least one fields`)
203 }
204
205 if (!areFieldsFilled.value) {
206 return new Error(`Please fill all fields`)
207 }
208
209 if (!areFieldsUniques.value) {
210 return new Error(`There are duplicated fields`)
211 }
212
213 return true
214 },
215 trigger: ["input", "blur"]
216 }
217 }
218
219 const isValid = computed(() => {
220 let valid = true
221
222 for (const key in rules) {
223 const rule = rules[key] as FormRules
224
225 if (rule.required && !_trim(_get(model.value, key))) {
226 valid = false
227 }
228 }
229
230 if (!areFieldsFilled.value || !areFieldsPresent.value || !areFieldsUniques.value) {
231 valid = false
232 }
233
234 return valid
235 })
236
237 function validate() {
238 if (!formRef.value) return
239
240 formRef.value.validate((errors?: Array<FormValidationError>) => {
241 if (!errors) {
242 submit()
243 } else {
244 message.warning("You must fill in the required fields correctly.")
245 return false
246 }
247 })
248 }
249
250 function getDefaultModel(entity?: Partial<ExclusionRule>): Model {
251 return {
252 name: entity?.name || "",
253 description: entity?.description || "",
254 channel: entity?.channel || "",
255 title: entity?.title || "",
256 field_matches: entity?.field_matches
257 ? Object.entries(entity.field_matches).map(o => ({ key: o[0], value: o[1], id: o[0] }))
258 : [{ id: `${Date.now()}`, key: null, value: null }],
259 customer_code: entity?.customer_code || undefined,
260 enabled: entity?.enabled || false
261 }
262 }
263
264 function reset(force?: boolean) {
265 if (!loading.value || force) {
266 setModel()
267 formRef.value?.restoreValidation()
268 }
269 }
270
271 function addField() {
272 model.value.field_matches.push({
273 id: `${Date.now()}`,
274 key: null,
275 value: null
276 })
277 }
278
279 function delField(index: number) {
280 model.value.field_matches.splice(index, 1)
281
282 if (!model.value.field_matches.length) {
283 addField()
284 }
285 }
286
287 function getCustomers() {
288 loadingCustomers.value = true
289
290 Api.customers
291 .getCustomers()
292 .then(res => {
293 if (res.data.success) {
294 customersList.value = res.data?.customers || []
295 } else {
296 message.warning(res.data?.message || "An error occurred. Please try again later.")
297 }
298 })
299 .catch(err => {
300 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
301 })
302 .finally(() => {
303 loadingCustomers.value = false
304 })
305 }
306
307 function submit() {
308 loading.value = true
309
310 const payload: ExclusionRulePayload = {
311 ...model.value,
312 field_matches: model.value.field_matches
313 .filter(o => !!o.key && !!o.value)
314 .reduce((acc: Record<string, string>, cur: FieldMatch) => {
315 acc[`${cur.key}`] = `${cur.value}`
316 return acc
317 }, {})
318 }
319
320 const method = entity.value?.id
321 ? Api.incidentManagement.exclusionRules.updateExclusionRule(entity.value.id, payload)
322 : Api.incidentManagement.exclusionRules.createExclusionRule(payload)
323
324 method
325 .then(res => {
326 if (res.data.success) {
327 emit("submitted", res.data.exclusion_response)
328 if (resetOnSubmit.value) {
329 reset(true)
330 }
331 } else {
332 message.warning(res.data?.message || "An error occurred. Please try again later.")
333 }
334 })
335 .catch(err => {
336 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
337 })
338 .finally(() => {
339 loading.value = false
340 })
341 }
342
343 function setModel() {
344 model.value = getDefaultModel(entity.value)
345 }
346
347 watch(loading, val => {
348 emit("update:loading", val)
349 })
350
351 watch(
352 entity,
353 val => {
354 if (val) {
355 setModel()
356 }
357 },
358 { immediate: true }
359 )
360
361 onBeforeMount(() => {
362 getCustomers()
363 })
364
365 onMounted(() => {
366 emit("mounted", {
367 reset
368 })
369 })
370 </script>