PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / stores / useNotificationSettingBuilder.ts

useNotificationSettingBuilder.ts in The Innovative Form Builder – IvyForms 1.1, at frontend/src/stores/useNotificationSettingBuilder.ts

466 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { defineStore } from 'pinia'
2 import { ref, watch, nextTick } from 'vue'
3 import { useLabels } from '@/composables/useLabels'
4 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
5 import { useApiClient } from '@/composables/useApiClient'
6 import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore'
7 import { getErrorMessage } from '@/utils/errorHandling'
8
9 export const useNotificationSettingBuilder = defineStore('notificationSettingBuilder', () => {
10 type NotificationSmartLogicRule = {
11 id: number
12 field: string | null
13 operator: 'equals' | 'not_equals'
14 value: string
15 }
16
17 type NotificationSmartLogic = {
18 enabled: boolean
19 match: 'any' | 'all'
20 rules: NotificationSmartLogicRule[]
21 }
22
23 const getDefaultSmartLogic = (): NotificationSmartLogic => ({
24 enabled: false,
25 match: 'any',
26 rules: [],
27 })
28
29 /**
30 * Merge API payload into the full smartLogic shape. Avoids replacing with defaults when the
31 * response omits the key (falsy ||) and loses saved rules; if `enabled` is missing but `rules`
32 * are present, treat logic as on (matches typical saved state / partial payloads).
33 */
34 const mergeSmartLogicFromResponse = (raw: unknown): NotificationSmartLogic => {
35 const defaults = getDefaultSmartLogic()
36 if (raw === null || raw === undefined) {
37 return defaults
38 }
39 if (typeof raw !== 'object' || Array.isArray(raw)) {
40 return defaults
41 }
42 const incoming = raw as Partial<NotificationSmartLogic>
43 const rules = Array.isArray(incoming.rules) ? incoming.rules : defaults.rules
44 const hasRules = rules.length > 0
45 const enabled =
46 typeof incoming.enabled === 'boolean' ? incoming.enabled : hasRules ? true : defaults.enabled
47 const match =
48 incoming.match === 'all' || incoming.match === 'any' ? incoming.match : defaults.match
49
50 return {
51 enabled,
52 match,
53 rules,
54 }
55 }
56
57 const allNotifications = ref([])
58 const { getLabel } = useLabels()
59 const tableData = ref([])
60 const paginationMeta = ref({
61 page: 1,
62 perPage: 10,
63 total: 0,
64 })
65 const name = ref<string | null>(getLabel('new_notification'))
66 const sender = ref<string | null>('')
67 const replyTo = ref<string | null>('')
68 const receiver = ref<string | null>('')
69 const enabled = ref(false)
70 const showEmptyFields = ref(false)
71 const subject = ref<string | null>('')
72 const message = ref<string | null>('')
73 const smartLogic = ref<NotificationSmartLogic>(getDefaultSmartLogic())
74 const formId = ref<string | null>(null)
75 const id = ref<number | null>(null)
76 const isEditing = ref(false) // Make `isEditing` reactive
77
78 // Use unsaved changes store for state management
79 const unsavedChangesStore = useUnsavedChangesStore()
80
81 // Flag to suppress change tracking during load/reset operations
82 let suppressDirtyTracking = false
83
84 /**
85 * Mark the notification builder as having unsaved changes
86 */
87 const markDirty = () => {
88 if (!suppressDirtyTracking && isEditing.value) {
89 unsavedChangesStore.markDirty('notificationBuilder')
90 }
91 }
92
93 /**
94 * Mark the notification builder as clean (no unsaved changes)
95 */
96 const markClean = () => {
97 unsavedChangesStore.markClean('notificationBuilder')
98 }
99
100 // Watch all tracked state for changes and mark dirty (deep: nested smartLogic.rules, etc.)
101 watch(
102 [name, sender, replyTo, receiver, enabled, showEmptyFields, subject, message, smartLogic],
103 () => markDirty(),
104 { deep: true },
105 )
106
107 const { request } = useApiClient()
108
109 // Load existing notification if notification id is provided
110 const loadNotification = async (NotifId: number) => {
111 // Suppress dirty tracking during load
112 suppressDirtyTracking = true
113
114 try {
115 const { data, error, status } = await request(`/notification/${NotifId}`, {
116 method: 'GET',
117 })
118 if (!error && status === 200 && data?.data?.data) {
119 const responseData = data.data.data
120 formId.value = responseData.formId
121 id.value = NotifId
122 name.value = responseData.name || getLabel('new_notification')
123 sender.value = responseData.sender || ''
124 replyTo.value = responseData.replyTo || ''
125 receiver.value = responseData.receiver || ''
126 enabled.value = responseData.enabled || false
127 showEmptyFields.value = Boolean(responseData.showEmptyFields)
128 subject.value = responseData.subject || getLabel('new_notification')
129 message.value = responseData.message || ''
130 smartLogic.value = mergeSmartLogicFromResponse(responseData.smartLogic)
131 isEditing.value = true
132
133 // Start editing and mark clean (just loaded)
134 unsavedChangesStore.startEditing('notificationBuilder')
135 markClean()
136 } else {
137 IvyMessage({
138 message: `${getLabel('failed_to_load_notification')} ${data?.message || error || ''}`,
139 type: 'error',
140 })
141 }
142 } catch (error) {
143 const errorMessage = getErrorMessage(error)
144 IvyMessage({
145 message: `${getLabel('failed_to_load_notification')} ${errorMessage}`,
146 type: 'error',
147 })
148 // Reset editing state on error
149 isEditing.value = false
150 unsavedChangesStore.stopEditing('notificationBuilder')
151 } finally {
152 // Wait for watchers to fire before re-enabling dirty tracking
153 await nextTick()
154 suppressDirtyTracking = false
155 }
156 }
157
158 // Fetch all form notifications
159 const fetchData = async (formId?: number) => {
160 try {
161 const url = formId ? `/notifications/${formId}` : `/notifications`
162 const { data, error } = await request(url, { method: 'GET' })
163 if (data && data.data) {
164 allNotifications.value = data.data.data
165 } else {
166 allNotifications.value = []
167 IvyMessage({
168 message: `${getLabel('unexpected_response_structure')} ${error || data || ''}`,
169 type: 'error',
170 })
171 }
172 } catch (error) {
173 const errorMessage = getErrorMessage(error)
174 IvyMessage({
175 message: `${getLabel('failed_to_fetch_notification')} ${errorMessage}`,
176 type: 'error',
177 })
178 }
179 }
180
181 // Fetch all notifications
182 const fetchAllNotifications = async () => {
183 try {
184 const url = `/notifications`
185 const { data, error } = await request(url, { method: 'GET' })
186 if (data && data.data) {
187 allNotifications.value = data.data.data
188 } else {
189 allNotifications.value = []
190 IvyMessage({
191 message: `${getLabel('unexpected_response_structure')} ${error || data || ''}`,
192 type: 'error',
193 })
194 }
195 } catch (error) {
196 const errorMessage = getErrorMessage(error)
197 IvyMessage({
198 message: `${getLabel('failed_to_fetch_notification')} ${errorMessage}`,
199 type: 'error',
200 })
201 }
202 }
203
204 // Reset notification to default values
205 const resetNotification = (formIdRoute: string) => {
206 // Suppress dirty tracking during reset
207 suppressDirtyTracking = true
208
209 name.value = getLabel('new_notification')
210 sender.value = ''
211 replyTo.value = ''
212 receiver.value = ''
213 enabled.value = true
214 showEmptyFields.value = false
215 subject.value = getLabel('new_notification')
216 message.value = '{{all_data}}'
217 smartLogic.value = getDefaultSmartLogic()
218 formId.value = formIdRoute
219 id.value = null
220 isEditing.value = false // Reset to false when resetting notification
221
222 // Stop editing and clear dirty flag
223 unsavedChangesStore.stopEditing('notificationBuilder')
224
225 // Re-enable dirty tracking
226 suppressDirtyTracking = false
227 }
228
229 // Create a new notification
230 const createNotification = async (formId) => {
231 const notificationData = {
232 name: name.value || getLabel('new_notification'),
233 sender: sender.value || '',
234 replyTo: replyTo.value || '',
235 receiver: receiver.value || '',
236 enabled: enabled.value || false,
237 showEmptyFields: showEmptyFields.value || false,
238 subject: subject.value || getLabel('new_notification'),
239 message: message.value || '',
240 smartLogic: smartLogic.value,
241 formId: formId || null,
242 }
243
244 try {
245 const { data, error, status } = await request('/notification/add/', {
246 method: 'POST',
247 data: notificationData,
248 })
249 if (!error && status === 200) {
250 id.value = data.data.data.id
251 allNotifications.value.push(data.data)
252 IvyMessage({
253 message: getLabel('create_notification'),
254 type: 'success',
255 })
256 markClean()
257 } else {
258 IvyMessage({
259 message: `${getLabel('failed_to_create_notification')}: ${data?.message || error || ''}`,
260 type: 'error',
261 })
262 }
263 } catch (error) {
264 const errorMessage = getErrorMessage(error)
265 IvyMessage({
266 message: `${getLabel('error_creating_notification')} ${errorMessage}`,
267 type: 'error',
268 })
269 }
270 }
271
272 // Update notification data
273 const updateNotification = async (NotifId, dataObj) => {
274 try {
275 const { data, error, status } = await request(`/notification/update/${NotifId}/`, {
276 method: 'POST',
277 data: dataObj,
278 })
279 if (!error && status === 200) {
280 IvyMessage({
281 message: getLabel('update_notification'),
282 type: 'success',
283 })
284 markClean()
285 } else {
286 IvyMessage({
287 message: `${getLabel('failed_to_update_notification')}: ${data?.message || error || ''}`,
288 type: 'error',
289 })
290 }
291 } catch (error) {
292 const errorMessage = getErrorMessage(error)
293 IvyMessage({
294 message: `${getLabel('error_updating_notification')} ${errorMessage}`,
295 type: 'error',
296 })
297 }
298 }
299
300 // Search notifications with pagination and sorting
301 const searchNotifications = async ({
302 page = 1,
303 perPage = 10,
304 search = '',
305 sortBy = '',
306 sortOrder = 'asc',
307 filters = {},
308 formId = null,
309 }: {
310 page?: number
311 perPage?: number
312 search?: string
313 sortBy?: string
314 sortOrder?: string
315 filters?: Record<string, string | number | null>
316 formId?: number | null
317 } = {}) => {
318 try {
319 const mergedFilters = { ...filters }
320 if (formId !== null && formId !== undefined) {
321 mergedFilters.formId = formId
322 }
323 const params: Record<
324 string,
325 string | number | boolean | null | Record<string, string | number | null>
326 > = {
327 page,
328 perPage,
329 search,
330 orderBy: sortBy,
331 order: sortOrder,
332 filters: mergedFilters,
333 }
334
335 const { data, error } = await request('notifications/search', {
336 method: 'GET',
337 params,
338 })
339
340 if (error) {
341 const errorMessage = getErrorMessage(error)
342 IvyMessage({
343 message: `${getLabel('error_fetching_notifications')} ${errorMessage}`,
344 type: 'error',
345 })
346 return
347 }
348
349 const response = data.data
350 tableData.value = normalizeNotificationData(response.data.data || [])
351 paginationMeta.value = response.data.meta || { page: 1, perPage: 10, total: 0 }
352 } catch (error) {
353 const errorMessage = getErrorMessage(error)
354 IvyMessage({
355 message: `${getLabel('error_fetching_notifications')} ${errorMessage}`,
356 type: 'error',
357 })
358 }
359 }
360
361 // Define proper types for notification data
362 interface RawNotificationItem {
363 id?: number
364 name?: string
365 sender?: string
366 receiver?: string
367 subject?: string
368 message?: string
369 enabled?: string | number | boolean
370 formId?: number
371 [key: string]: unknown // For any wrapped notification properties
372 }
373
374 interface NormalizedNotification {
375 id: number
376 name: string
377 sender: string
378 receiver: string
379 subject: string
380 message: string
381 enabled: boolean
382 formId: number
383 }
384
385 // Normalize notification data structure
386 const normalizeNotificationData = (data: RawNotificationItem[]): NormalizedNotification[] => {
387 return data.map((item) => {
388 if (item && typeof item === 'object') {
389 const notificationKey = Object.keys(item).find(
390 (key) => key.includes('notification') || key.includes('Notification'),
391 )
392
393 if (notificationKey && item[notificationKey]) {
394 const notification = item[notificationKey] as RawNotificationItem
395 return {
396 id: notification.id || 0,
397 name: notification.name || '',
398 sender: notification.sender || '',
399 replyTo: notification.replyTo || '',
400 receiver: notification.receiver || '',
401 subject: notification.subject || '',
402 message: notification.message || '',
403 enabled: notification.enabled === '1',
404 formId: notification.formId || 0,
405 }
406 }
407
408 // If it's a direct notification object
409 if (item.id && item.name !== undefined) {
410 return {
411 id: item.id,
412 name: item.name,
413 sender: item.sender || '',
414 replyTo: item.replyTo || '',
415 receiver: item.receiver || '',
416 subject: item.subject || '',
417 message: item.message || '',
418 enabled: item.enabled === '1',
419 formId: item.formId || null,
420 }
421 }
422 }
423
424 // Return a fallback object for items that couldn't be normalized
425 return {
426 id: 0,
427 name: 'Unknown',
428 sender: '',
429 replyTo: '',
430 receiver: '',
431 subject: '',
432 message: '',
433 enabled: false,
434 formId: null,
435 }
436 })
437 }
438
439 return {
440 allNotifications,
441 tableData,
442 paginationMeta,
443 name,
444 sender,
445 replyTo,
446 receiver,
447 enabled,
448 showEmptyFields,
449 subject,
450 message,
451 smartLogic,
452 formId,
453 id,
454 isEditing,
455 markDirty,
456 markClean,
457 fetchData,
458 fetchAllNotifications,
459 resetNotification,
460 createNotification,
461 updateNotification,
462 loadNotification,
463 searchNotifications,
464 }
465 })
466