@dev.smartpricing/message-composer-layer 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,42 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import type { MediaType } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { MEDIA_LIMITS } from '@dev.smartpricing/message-composer-utils/types'
4
+
5
+ export function useMediaLimits(mediaType: MaybeRefOrGetter<MediaType>) {
6
+ const { t } = useI18n()
7
+
8
+ const limits = computed(() => MEDIA_LIMITS[toValue(mediaType)])
9
+
10
+ const accept = computed(() => limits.value.accept.join(','))
11
+
12
+ const maxSize = computed(() => limits.value.maxSize)
13
+
14
+ const limitationsText = computed(() => {
15
+ const type = toValue(mediaType)
16
+ const sizeMB = limits.value.maxSize / (1024 * 1024)
17
+ return t(`common.imageUpload.mediaLimitations.${type}`, { maxSize: sizeMB })
18
+ })
19
+
20
+ function validateFile(file: File): boolean {
21
+ if (!limits.value.accept.includes(file.type)) {
22
+ useToast().add({
23
+ color: 'error',
24
+ title: t('common.imageUpload.mediaInvalidType'),
25
+ })
26
+ return false
27
+ }
28
+
29
+ if (file.size > limits.value.maxSize) {
30
+ const sizeMB = limits.value.maxSize / (1024 * 1024)
31
+ useToast().add({
32
+ color: 'error',
33
+ title: t('common.imageUpload.mediaFileTooLarge', { maxSize: sizeMB }),
34
+ })
35
+ return false
36
+ }
37
+
38
+ return true
39
+ }
40
+
41
+ return { accept, maxSize, limitationsText, validateFile }
42
+ }
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod'
2
+ import { computed, ref, type Ref, type ComputedRef } from 'vue'
3
+ import type {
4
+ EmailMessage,
5
+ MessageFromDb,
6
+ MessageGroupCategory,
7
+ PostEntity,
8
+ WhatsappMessage,
9
+ } from '@dev.smartpricing/message-composer-utils/types'
10
+ import { isEmailValid, isWhatsappValid } from '@dev.smartpricing/message-composer-utils/utils'
11
+
12
+ interface UseTemplateFormValidationOptions {
13
+ type: 'email' | 'whatsapp'
14
+ name: Ref<string> | ComputedRef<string>
15
+ category: Ref<MessageGroupCategory> | ComputedRef<MessageGroupCategory>
16
+ messages: Ref<Record<string, PostEntity<EmailMessage> | WhatsappMessage>>
17
+ }
18
+
19
+ const draftSchema = z.object({
20
+ name: z.string().min(1, 'editor.validation.error.nameRequired'),
21
+ category: z.string().min(1),
22
+ messages: z.record(z.string(), z.any()).refine((messages) => 'en' in messages, {
23
+ message: 'editor.validation.error.messageRequired',
24
+ }),
25
+ })
26
+
27
+ export function useTemplateFormValidation(options: UseTemplateFormValidationOptions) {
28
+ const { type, name, category, messages } = options
29
+ const { t } = useI18n()
30
+ const emailEditorStore = useEmailEditorStore()
31
+
32
+ const formRef = useTemplateRef('templateForm')
33
+ const validationErrors = ref<string[]>([])
34
+
35
+ const formState = computed(() => ({
36
+ name: name.value,
37
+ category: category.value,
38
+ messages: messages.value,
39
+ }))
40
+
41
+ function clearErrors() {
42
+ validationErrors.value = []
43
+ formRef.value?.clear()
44
+ }
45
+
46
+ async function validateDraft(): Promise<boolean> {
47
+ clearErrors()
48
+ try {
49
+ await formRef.value?.validate()
50
+ return true
51
+ } catch {
52
+ const formErrors = formRef.value?.errors
53
+ const errors: string[] = []
54
+ if (Array.isArray(formErrors)) {
55
+ for (const err of formErrors) {
56
+ if (err.message) {
57
+ errors.push(t(err.message))
58
+ }
59
+ }
60
+ }
61
+ validationErrors.value = errors
62
+ return false
63
+ }
64
+ }
65
+
66
+ async function validateFull(): Promise<boolean> {
67
+ const draftValid = await validateDraft()
68
+ if (!draftValid) return false
69
+
70
+ const errors: string[] = []
71
+ const messageEntries = Object.entries(messages.value)
72
+ const hasMultipleLanguages = messageEntries.length > 1
73
+ for (const [langId, message] of messageEntries) {
74
+ const result =
75
+ type === 'email'
76
+ ? isEmailValid(message as unknown as EmailMessage, emailEditorStore.brand)
77
+ : isWhatsappValid(message as unknown as MessageFromDb)
78
+ if (!result.valid) {
79
+ const uniqueErrors = [...new Set(result.errors)]
80
+ for (const error of uniqueErrors) {
81
+ const translated = t(error)
82
+ if (hasMultipleLanguages) {
83
+ errors.push(`${langId.toUpperCase()}: ${translated}`)
84
+ } else {
85
+ errors.push(translated)
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ if (errors.length > 0) {
92
+ validationErrors.value = errors
93
+ formRef.value?.setErrors(errors.map((message) => ({ message, name: 'messages' })))
94
+ return false
95
+ }
96
+
97
+ return true
98
+ }
99
+
100
+ return {
101
+ formRef,
102
+ formState,
103
+ draftSchema,
104
+ validationErrors,
105
+ validateDraft,
106
+ validateFull,
107
+ clearErrors,
108
+ }
109
+ }
@@ -0,0 +1,147 @@
1
+ import { ref } from 'vue'
2
+ import {
3
+ useCreateMessageGroupMutation,
4
+ useUpdateMessageGroupMutation,
5
+ useCreateMessageMutation,
6
+ useUpdateMessageMutation,
7
+ } from '~/queries'
8
+ import { messageGroupsApi } from '~/api'
9
+ import type {
10
+ MessageFromDb,
11
+ MessageGroupCategory,
12
+ Message,
13
+ PostEntity,
14
+ MessageStatus,
15
+ } from '@dev.smartpricing/message-composer-utils/types'
16
+
17
+ interface TemplateData<T = Message | PostEntity<Message>> {
18
+ messageGroup: { name: string; category: MessageGroupCategory; brand_id?: string | null }
19
+ messages: Record<string, T>
20
+ }
21
+
22
+ interface UseTemplateOperationsOptions {
23
+ onSuccess?: () => void
24
+ onError?: (error: Error) => void
25
+ }
26
+
27
+ function transformMessagesForCreation(
28
+ messages: MessageFromDb[],
29
+ ): Record<string, PostEntity<Message>> {
30
+ return messages.reduce(
31
+ (acc, message) => {
32
+ // Remove DB-specific fields, keep message group content
33
+
34
+ const { id, message_group_id, created_at, updated_at, ...messageGroupMessage } = message
35
+ acc[message.language_id] = { ...messageGroupMessage, message_group_id: '', metadata: {} }
36
+
37
+ return acc
38
+ },
39
+ {} as Record<string, PostEntity<Message>>,
40
+ )
41
+ }
42
+
43
+ export function useTemplateOperations(options: UseTemplateOperationsOptions = {}) {
44
+ const { t } = useI18n()
45
+ const saving = ref(false)
46
+ const saved = ref(false)
47
+
48
+ // Hoist composables to setup scope (they use inject() internally)
49
+ const { mutateAsync: createMessageGroup } = useCreateMessageGroupMutation()
50
+ const { mutateAsync: createMessage } = useCreateMessageMutation()
51
+ const { mutateAsync: updateMessageGroup } = useUpdateMessageGroupMutation()
52
+ const { mutateAsync: updateMessage } = useUpdateMessageMutation()
53
+
54
+ // Create new message group(used by Create + Duplicate)
55
+ async function createTemplate(data: TemplateData, status: MessageStatus = 'draft') {
56
+ saving.value = true
57
+ try {
58
+ const createdMessageGroup = await createMessageGroup(data.messageGroup)
59
+
60
+ if (createdMessageGroup?.id) {
61
+ for (const message of Object.values(data.messages)) {
62
+ message.message_group_id = createdMessageGroup.id
63
+ message.status = status
64
+
65
+ await createMessage(message)
66
+ }
67
+ }
68
+
69
+ saved.value = true
70
+ options.onSuccess?.()
71
+ return createdMessageGroup
72
+ } catch (error) {
73
+ options.onError?.(error as Error)
74
+ throw error
75
+ } finally {
76
+ saving.value = false
77
+ }
78
+ }
79
+
80
+ // Update existing message group (used by Edit)
81
+ async function updateTemplate(
82
+ id: string,
83
+ data: TemplateData<Message | PostEntity<Message>>,
84
+ status?: MessageStatus,
85
+ ) {
86
+ saving.value = true
87
+ try {
88
+ await updateMessageGroup({ id, data: data.messageGroup })
89
+
90
+ for (const message of Object.values(data.messages)) {
91
+ if (status) {
92
+ message.status = status
93
+ }
94
+
95
+ if ('id' in message) {
96
+ await updateMessage({ id: message.id, data: message })
97
+ } else {
98
+ message.message_group_id = id
99
+ await createMessage(message)
100
+ }
101
+ }
102
+
103
+ saved.value = true
104
+ options.onSuccess?.()
105
+ } catch (error) {
106
+ options.onError?.(error as Error)
107
+ throw error
108
+ } finally {
109
+ saving.value = false
110
+ }
111
+ }
112
+
113
+ // Duplicate existing message group (used by Duplicate action)
114
+ async function duplicateTemplate(sourceId: string, newName?: string) {
115
+ saving.value = true
116
+ try {
117
+ const sourceTemplate = await messageGroupsApi.getById(sourceId)
118
+
119
+ if (!sourceTemplate) {
120
+ throw new Error('Source message group not found')
121
+ }
122
+
123
+ const templateData: TemplateData = {
124
+ messageGroup: {
125
+ name: newName || t('common.copy_of', { name: sourceTemplate.name }),
126
+ category: sourceTemplate.category,
127
+ brand_id: sourceTemplate.brand_id,
128
+ },
129
+ messages: transformMessagesForCreation(sourceTemplate.messages),
130
+ }
131
+
132
+ const result = await createTemplate(templateData, 'draft')
133
+ return result
134
+ } catch (error) {
135
+ options.onError?.(error as Error)
136
+ throw error
137
+ }
138
+ }
139
+
140
+ return {
141
+ saving,
142
+ saved,
143
+ createTemplate,
144
+ updateTemplate,
145
+ duplicateTemplate,
146
+ }
147
+ }
@@ -0,0 +1,39 @@
1
+ import type { TrackingEventMap } from '~/types/tracking'
2
+
3
+ export function useTracking() {
4
+ const { product, mainLanguage } = storeToRefs(useAppContextStore())
5
+ const { isAdminModeActive } = storeToRefs(useAdminStore())
6
+
7
+ function getDefaultProperties(): Record<string, unknown> {
8
+ return {
9
+ event_origin: 'message-composer',
10
+ product: product.value,
11
+ main_language: mainLanguage.value,
12
+ is_admin: isAdminModeActive.value,
13
+ }
14
+ }
15
+
16
+ async function trackEvent<T extends keyof TrackingEventMap>(
17
+ eventName: T,
18
+ properties?: TrackingEventMap[T],
19
+ ): Promise<void> {
20
+ if (!useAppConfig().composer?.tracking) return
21
+ try {
22
+ const { trackingApi } = await import('@/api/tracking')
23
+ await trackingApi.event(eventName, {
24
+ ...getDefaultProperties(),
25
+ ...properties,
26
+ })
27
+ } catch {
28
+ if (import.meta.dev) {
29
+ console.warn(`[Tracking] Failed to track "${eventName}"`, properties)
30
+ }
31
+ }
32
+ }
33
+
34
+ function trackPageView(pageName: string): void {
35
+ trackEvent('Page View', { page_name: pageName })
36
+ }
37
+
38
+ return { trackEvent, trackPageView }
39
+ }
@@ -0,0 +1,176 @@
1
+ import { computed, type Ref, type ComputedRef } from 'vue'
2
+ import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { extractText, isTiptapEmpty } from '@dev.smartpricing/message-composer-utils/utils'
4
+ import { useChangeTracking } from './useChangeTracking'
5
+
6
+ type Section = 'header' | 'body' | 'footer' | 'buttons'
7
+
8
+ interface TranslationFiltersParams {
9
+ messages: Ref<Record<string, PostEntity<WhatsappMessage>>>
10
+ currentLanguage: Ref<string>
11
+ baseLanguage: Ref<string>
12
+ messageGroupId?: Ref<string | number | undefined>
13
+ searchQuery: Ref<string>
14
+ showOnlyMissing: Ref<boolean>
15
+ showOnlyChanges: Ref<boolean>
16
+ }
17
+
18
+ export function useTranslationFilters(params: TranslationFiltersParams) {
19
+ const {
20
+ messages,
21
+ currentLanguage,
22
+ baseLanguage,
23
+ messageGroupId,
24
+ searchQuery,
25
+ showOnlyMissing,
26
+ showOnlyChanges,
27
+ } = params
28
+
29
+ // Initialize change tracking if messageGroupId is provided
30
+ const changeTracking = computed(() => {
31
+ if (!messageGroupId?.value) return null
32
+ return useChangeTracking(messageGroupId.value)
33
+ })
34
+
35
+ /**
36
+ * Check if a section matches the search query
37
+ */
38
+ function matchesSearch(section: Section, baseMsg: PostEntity<WhatsappMessage>): boolean {
39
+ if (!searchQuery.value || !searchQuery.value.trim()) return true
40
+
41
+ const query = searchQuery.value.toLowerCase().trim()
42
+ let searchText = ''
43
+
44
+ switch (section) {
45
+ case 'header':
46
+ if (baseMsg.body?.header?.type === 'text' && baseMsg.body.header.content) {
47
+ searchText = extractText(baseMsg.body.header.content, baseMsg.render_type)
48
+ }
49
+ break
50
+ case 'body':
51
+ if (baseMsg.body?.body?.content) {
52
+ searchText = extractText(baseMsg.body.body.content, baseMsg.render_type)
53
+ }
54
+ break
55
+ case 'footer':
56
+ if (baseMsg.body?.footer?.content) {
57
+ searchText = extractText(baseMsg.body.footer.content, baseMsg.render_type)
58
+ }
59
+ break
60
+ case 'buttons':
61
+ if (baseMsg.body?.buttons?.buttons) {
62
+ searchText = baseMsg.body.buttons.buttons
63
+ .map((btn) => extractText(btn?.text || '', baseMsg.render_type))
64
+ .join(' ')
65
+ }
66
+ break
67
+ }
68
+
69
+ return searchText.toLowerCase().includes(query)
70
+ }
71
+
72
+ /**
73
+ * Check if a section has missing translations
74
+ */
75
+ function hasMissingTranslation(
76
+ section: Section,
77
+ targetMsg: PostEntity<WhatsappMessage>,
78
+ ): boolean {
79
+ if (!targetMsg.body) return true
80
+
81
+ switch (section) {
82
+ case 'header':
83
+ // Header is optional, only check if it exists in base
84
+ if (targetMsg.body.header?.type === 'text') {
85
+ return isTiptapEmpty(targetMsg.body.header.content, targetMsg.render_type)
86
+ }
87
+ if (['image', 'video', 'document'].includes(targetMsg.body.header?.type)) {
88
+ return !('url' in targetMsg.body.header) || !targetMsg.body.header.url
89
+ }
90
+ return false
91
+
92
+ case 'body':
93
+ // Body is required
94
+ return isTiptapEmpty(targetMsg.body.body?.content, targetMsg.render_type)
95
+
96
+ case 'footer':
97
+ // Footer is optional
98
+ return isTiptapEmpty(targetMsg.body.footer?.content, targetMsg.render_type)
99
+
100
+ case 'buttons':
101
+ // Check if any button has empty text
102
+ if (targetMsg.body.buttons?.buttons) {
103
+ return targetMsg.body.buttons.buttons.some((btn) =>
104
+ isTiptapEmpty(btn?.text, targetMsg.render_type),
105
+ )
106
+ }
107
+ return false
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Check if a section has changes
113
+ */
114
+ function hasChanges(section: Section): boolean {
115
+ const tracking = changeTracking.value
116
+ if (!tracking || !currentLanguage.value) return false
117
+
118
+ const currentMsg = messages.value[currentLanguage.value]
119
+ if (!currentMsg) return false
120
+
121
+ const changedFields = tracking.getChangedFields(
122
+ currentLanguage.value,
123
+ currentMsg as WhatsappMessage,
124
+ )
125
+ return changedFields.includes(section)
126
+ }
127
+
128
+ /**
129
+ * Filter sections based on active filters
130
+ */
131
+ const filteredSections: ComputedRef<Section[]> = computed(() => {
132
+ const baseMsg = messages.value[baseLanguage.value]
133
+ const targetMsg = messages.value[currentLanguage.value]
134
+
135
+ if (!baseMsg || !targetMsg) {
136
+ return []
137
+ }
138
+
139
+ const allSections: Section[] = ['header', 'body', 'footer', 'buttons']
140
+
141
+ return allSections.filter((section) => {
142
+ // Search filter
143
+ if (searchQuery.value && !matchesSearch(section, baseMsg)) {
144
+ return false
145
+ }
146
+
147
+ // Missing translations filter
148
+ if (showOnlyMissing.value && !hasMissingTranslation(section, targetMsg)) {
149
+ return false
150
+ }
151
+
152
+ // Changes filter
153
+ if (showOnlyChanges.value && !hasChanges(section)) {
154
+ return false
155
+ }
156
+
157
+ return true
158
+ })
159
+ })
160
+
161
+ /**
162
+ * Check if any filters are active
163
+ */
164
+ const hasActiveFilters: ComputedRef<boolean> = computed(() => {
165
+ return (
166
+ (searchQuery.value && searchQuery.value.trim() !== '') ||
167
+ showOnlyMissing.value ||
168
+ showOnlyChanges.value
169
+ )
170
+ })
171
+
172
+ return {
173
+ filteredSections,
174
+ hasActiveFilters,
175
+ }
176
+ }