@dev.smartpricing/message-composer-layer 4.2.3-validations.0 → 4.2.4-brands.0

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.
package/app/api/brands.ts CHANGED
@@ -21,7 +21,7 @@ export const brandsApi = {
21
21
  if (options?.sort_by) params.set('sort_by', options.sort_by)
22
22
  if (options?.direction) params.set('direction', options.direction)
23
23
  const query = params.size ? `?${params.toString()}` : ''
24
- return apiClient<BrandFromDb[]>(`/brands${query}`)
24
+ return apiClient<BrandWithProperties[]>(`/brands${query}`)
25
25
  },
26
26
 
27
27
  getById: (id: string) => apiClient<BrandWithProperties>(`/brands/${id}`),
@@ -6,13 +6,15 @@ import type {
6
6
  BrandDefaultButtonSettings,
7
7
  BrandCreateBody,
8
8
  BrandSocials,
9
+ BrandData,
9
10
  } from '@dev.smartpricing/message-composer-utils/types'
10
- import { isValidUrl } from '@dev.smartpricing/message-composer-utils/utils'
11
+ import { isValidUrl, isEmail } from '@dev.smartpricing/message-composer-utils/utils'
11
12
  import type { BrandCascadeData } from '@dev.smartpricing/message-composer-utils/utils'
12
13
  import type { BrandWithProperties } from '~/api/brands'
13
14
  import BrandPreview from '~/components/Brand/BrandPreview.vue'
14
15
  import BrandStepGeneral from '~/components/Brand/Steps/BrandStepGeneral.vue'
15
16
  import BrandStepVisual from '~/components/Brand/Steps/BrandStepVisual.vue'
17
+ import BrandStepContactInfo from '~/components/Brand/Steps/BrandStepContactInfo.vue'
16
18
  import BrandStepSocials from '~/components/Brand/Steps/BrandStepSocials.vue'
17
19
  import { useBrandQuery, useCreateBrandMutation, useUpdateBrandMutation } from '~/queries'
18
20
  import type { BrandCreateResponse } from '~/api/brands'
@@ -48,7 +50,7 @@ const isSaving = computed(() => isCreating.value || isUpdating.value)
48
50
  // ============================================================================
49
51
  // NAVIGATION
50
52
  // ============================================================================
51
- type Step = 'general' | 'visual' | 'socials'
53
+ type Step = 'general' | 'visual' | 'contact_info' | 'socials'
52
54
  const activeStep = ref<Step>('general')
53
55
 
54
56
  const navItems = computed<NavigationMenuItem[]>(() => [
@@ -68,6 +70,14 @@ const navItems = computed<NavigationMenuItem[]>(() => [
68
70
  activeStep.value = 'visual'
69
71
  },
70
72
  },
73
+ {
74
+ label: t('pages.brands.steps.contact_info'),
75
+ icon: 'ph:address-book',
76
+ active: activeStep.value === 'contact_info',
77
+ onSelect: () => {
78
+ activeStep.value = 'contact_info'
79
+ },
80
+ },
71
81
  {
72
82
  label: t('pages.brands.steps.socials'),
73
83
  icon: 'ph:share-network',
@@ -99,6 +109,7 @@ const defaultButtonSettings = ref<BrandDefaultButtonSettings>({
99
109
  backgroundColor: '#000000',
100
110
  })
101
111
 
112
+ const data = ref<BrandData>({})
102
113
  const socials = ref<BrandSocials>({})
103
114
 
104
115
  // ============================================================================
@@ -124,6 +135,7 @@ function populateForm(brand: BrandWithProperties) {
124
135
  colors.value = [...brand.colors]
125
136
  defaultEmailSettings.value = { ...brand.default_email_settings }
126
137
  defaultButtonSettings.value = { ...brand.default_button_settings }
138
+ data.value = brand.data ? { ...brand.data } : {}
127
139
  socials.value = brand.default_socials ? { ...brand.default_socials } : {}
128
140
  }
129
141
 
@@ -143,6 +155,7 @@ function resetForm() {
143
155
  textColor: '#FFFFFF',
144
156
  backgroundColor: '#000000',
145
157
  }
158
+ data.value = {}
146
159
  socials.value = {}
147
160
  activeStep.value = 'general'
148
161
  }
@@ -151,6 +164,13 @@ watch(isOpen, (val) => {
151
164
  if (!val) resetForm()
152
165
  })
153
166
 
167
+ // Pre-fill data.name with brand display name on create only
168
+ watch(activeStep, (step) => {
169
+ if (step === 'contact_info' && !isEditMode.value && !data.value.name && name.value.trim()) {
170
+ data.value = { ...data.value, name: name.value.trim() }
171
+ }
172
+ })
173
+
154
174
  // ============================================================================
155
175
  // PREVIEW DATA
156
176
  // ============================================================================
@@ -159,6 +179,7 @@ const brandCascadeData = computed<BrandCascadeData>(() => ({
159
179
  default_button_settings: defaultButtonSettings.value,
160
180
  default_socials: socials.value,
161
181
  logo_url: logoUrl.value || null,
182
+ data: data.value,
162
183
  }))
163
184
 
164
185
  // ============================================================================
@@ -168,6 +189,13 @@ function hasInvalidSocialUrls(): boolean {
168
189
  return Object.values(socials.value).some((url) => !!url && !isValidUrl(url))
169
190
  }
170
191
 
192
+ function hasInvalidContactInfo(): boolean {
193
+ const d = data.value
194
+ if (d.website && !isValidUrl(d.website)) return true
195
+ if (d.email && !isEmail(d.email)) return true
196
+ return false
197
+ }
198
+
171
199
  async function handleSave() {
172
200
  if (!name.value.trim()) {
173
201
  useToast().add({
@@ -178,6 +206,15 @@ async function handleSave() {
178
206
  return
179
207
  }
180
208
 
209
+ if (hasInvalidContactInfo()) {
210
+ useToast().add({
211
+ color: 'error',
212
+ title: t('pages.brands.validation.invalid_contact_info'),
213
+ })
214
+ activeStep.value = 'contact_info'
215
+ return
216
+ }
217
+
181
218
  if (hasInvalidSocialUrls()) {
182
219
  useToast().add({
183
220
  color: 'error',
@@ -194,6 +231,7 @@ async function handleSave() {
194
231
  default_email_settings: defaultEmailSettings.value,
195
232
  default_button_settings: defaultButtonSettings.value,
196
233
  default_socials: socials.value,
234
+ data: data.value,
197
235
  property_ids: selectedPropertyIds.value,
198
236
  }
199
237
 
@@ -221,6 +259,7 @@ async function handleSave() {
221
259
  changedFields.push('button_settings')
222
260
  if (JSON.stringify(payload.default_socials) !== JSON.stringify(brand.default_socials))
223
261
  changedFields.push('socials')
262
+ if (JSON.stringify(payload.data) !== JSON.stringify(brand.data)) changedFields.push('data')
224
263
  if (
225
264
  JSON.stringify(payload.property_ids) !==
226
265
  JSON.stringify(brand.properties?.map((bp) => bp.property_id))
@@ -291,6 +330,7 @@ async function handleSave() {
291
330
  v-if="activeStep === 'general'"
292
331
  v-model:name="name"
293
332
  v-model:selected-property-ids="selectedPropertyIds"
333
+ :brand-id="brandId"
294
334
  />
295
335
 
296
336
  <BrandStepVisual
@@ -302,6 +342,8 @@ async function handleSave() {
302
342
  v-model:default-button-settings="defaultButtonSettings"
303
343
  />
304
344
 
345
+ <BrandStepContactInfo v-if="activeStep === 'contact_info'" v-model="data" />
346
+
305
347
  <BrandStepSocials v-if="activeStep === 'socials'" v-model="socials" />
306
348
  </div>
307
349
 
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import type { BrandData } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { isValidUrl, isEmail } from '@dev.smartpricing/message-composer-utils/utils'
4
+
5
+ const data = defineModel<BrandData>({ required: true })
6
+
7
+ const { t, locale } = useI18n()
8
+
9
+ const touched = ref<Partial<Record<string, boolean>>>({})
10
+
11
+ function update(field: keyof BrandData, value: string) {
12
+ const trimmed = value.trim()
13
+ data.value = {
14
+ ...data.value,
15
+ [field]: trimmed || undefined,
16
+ }
17
+ }
18
+
19
+ function onBlur(field: string) {
20
+ const value = data.value[field as keyof BrandData]
21
+ if (value) {
22
+ touched.value = { ...touched.value, [field]: true }
23
+ }
24
+ }
25
+
26
+ function fieldError(field: string): string | undefined {
27
+ const value = data.value[field as keyof BrandData]
28
+ if (!value || !touched.value[field]) return undefined
29
+
30
+ if (field === 'website') {
31
+ return isValidUrl(value) ? undefined : t('pages.brands.validation.invalid_url_format')
32
+ }
33
+ if (field === 'email') {
34
+ return isEmail(value) ? undefined : t('pages.brands.validation.invalid_email_format')
35
+ }
36
+ return undefined
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="space-y-4">
42
+ <UFormField :label="t('pages.brands.fields.contact_name')">
43
+ <UInput
44
+ :model-value="data.name ?? ''"
45
+ :placeholder="t('pages.brands.fields.contact_name_placeholder')"
46
+ @update:model-value="update('name', $event as string)"
47
+ />
48
+ </UFormField>
49
+
50
+ <UFormField :label="t('pages.brands.fields.contact_address')">
51
+ <UInput
52
+ :model-value="data.address ?? ''"
53
+ :placeholder="t('pages.brands.fields.contact_address_placeholder')"
54
+ @update:model-value="update('address', $event as string)"
55
+ />
56
+ </UFormField>
57
+
58
+ <UFormField :label="t('pages.brands.fields.contact_telephone')">
59
+ <SPhoneInput
60
+ class="w-full"
61
+ :model-value="data.telephone ?? ''"
62
+ :locale="locale"
63
+ @update:model-value="update('telephone', $event as string)"
64
+ />
65
+ </UFormField>
66
+
67
+ <UFormField :label="t('pages.brands.fields.contact_email')" :error="fieldError('email')">
68
+ <UInput
69
+ :model-value="data.email ?? ''"
70
+ :placeholder="t('pages.brands.fields.contact_email_placeholder')"
71
+ :color="fieldError('email') ? 'error' : undefined"
72
+ @update:model-value="update('email', $event as string)"
73
+ @blur="onBlur('email')"
74
+ />
75
+ </UFormField>
76
+
77
+ <UFormField :label="t('pages.brands.fields.contact_website')" :error="fieldError('website')">
78
+ <UInput
79
+ :model-value="data.website ?? ''"
80
+ :placeholder="t('pages.brands.fields.contact_website_placeholder')"
81
+ :color="fieldError('website') ? 'error' : undefined"
82
+ @update:model-value="update('website', $event as string)"
83
+ @blur="onBlur('website')"
84
+ />
85
+ </UFormField>
86
+ </div>
87
+ </template>
@@ -1,16 +1,37 @@
1
1
  <script setup lang="ts">
2
2
  import type { Property } from '@dev.smartpricing/message-composer-utils/types'
3
- import { usePropertiesQuery } from '~/queries'
3
+ import { useBrandsQuery, usePropertiesQuery } from '~/queries'
4
+
5
+ const props = defineProps<{
6
+ brandId?: string
7
+ }>()
4
8
 
5
9
  const name = defineModel<string>('name', { required: true })
6
10
  const selectedPropertyIds = defineModel<string[]>('selectedPropertyIds', { required: true })
7
11
 
12
+ const { t } = useI18n()
13
+
8
14
  const { data: properties } = usePropertiesQuery()
15
+ const { data: brands } = useBrandsQuery()
16
+
17
+ /** Map of property_id → brand name, excluding the brand currently being edited */
18
+ const takenPropertyMap = computed(() => {
19
+ const map = new Map<string, string>()
20
+ for (const brand of brands.value ?? []) {
21
+ if (brand.id === props.brandId) continue
22
+ for (const bp of brand.properties ?? []) {
23
+ map.set(bp.property_id, brand.name)
24
+ }
25
+ }
26
+ return map
27
+ })
9
28
 
10
29
  const propertyItems = computed(() =>
11
30
  (properties.value ?? []).map((p: Property) => ({
12
31
  label: p.name,
13
32
  value: p.id,
33
+ disabled: takenPropertyMap.value.has(p.id),
34
+ takenByBrand: takenPropertyMap.value.get(p.id),
14
35
  })),
15
36
  )
16
37
  </script>
@@ -24,7 +45,7 @@ const propertyItems = computed(() =>
24
45
  />
25
46
  </UFormField>
26
47
 
27
- <!-- <UFormField :label="$t('pages.brands.fields.properties')">
48
+ <UFormField :label="$t('pages.brands.fields.properties')">
28
49
  <USelectMenu
29
50
  v-model="selectedPropertyIds"
30
51
  :items="propertyItems"
@@ -32,6 +53,18 @@ const propertyItems = computed(() =>
32
53
  value-key="value"
33
54
  :placeholder="$t('pages.brands.fields.properties_placeholder')"
34
55
  class="w-full"
35
- />
36
- </UFormField> -->
56
+ >
57
+ <template #item-label="{ item }">
58
+ <UTooltip
59
+ v-if="item.disabled"
60
+ :text="t('pages.brands.fields.property_taken_tooltip', { brand: item.takenByBrand })"
61
+ >
62
+ <span class="text-muted">
63
+ {{ item.label }}
64
+ </span>
65
+ </UTooltip>
66
+ <span v-else>{{ item.label }}</span>
67
+ </template>
68
+ </USelectMenu>
69
+ </UFormField>
37
70
  </template>