فهرست منبع

Merge branch 'feature/V8-7398-ajout-des-lments-lis--la-demande' into develop

Olivier Massot 4 ماه پیش
والد
کامیت
06c45a4caf

+ 56 - 0
components/Common/PhoneInput.vue

@@ -0,0 +1,56 @@
+<template>
+  <v-text-field
+    :model-value="modelValue"
+    :rules="[validateRequired, validatePhone]"
+    :label="label"
+    :required="required"
+    type="tel"
+    @update:model-value="updateValue"
+  />
+</template>
+
+<script setup lang="ts">
+import { isValidPhoneNumber } from 'libphonenumber-js'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: '',
+  },
+  label: {
+    type: String,
+    default: 'Téléphone',
+  },
+  required: {
+    type: Boolean,
+    default: false,
+  },
+  countryCode: {
+    type: String,
+    default: 'FR',
+  },
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const updateValue = (value: string) => {
+  emit('update:modelValue', value)
+}
+
+// Validation rules
+const validateRequired = (value: string) =>
+  !props.required || !!value || 'Ce champ est obligatoire'
+
+const validatePhone = (phone: string) => {
+  if (!phone) return true
+  try {
+    // Use the provided country code or default to FR
+    return (
+      isValidPhoneNumber(phone, props.countryCode) ||
+      'Numéro de téléphone invalide'
+    )
+  } catch (error) {
+    return 'Numéro de téléphone invalide'
+  }
+}
+</script>

+ 1 - 3
components/Common/Table/Comparatif.vue

@@ -181,9 +181,7 @@
         </div>
 
         <div class="asterisk">
-          <span>
-            * Tarif public 2025. Abonnement payable annuellement.
-          </span>
+          <span> * Tarif public 2025. Abonnement payable annuellement. </span>
         </div>
       </v-row>
     </div>

+ 20 - 2
components/Home/Caroussel.vue

@@ -34,6 +34,16 @@ Carrousel de la page d'accueil
             >
               En savoir plus
             </v-btn>
+
+            <v-btn
+              v-if="item.link === 'opentalent-artist'"
+              to="/shop/try/artist-premium"
+              append-icon="fas fa-arrow-right"
+              :class="['mt-3 mb-3 artist-premium-btn']"
+              :style="{ backgroundColor: item.color }"
+            >
+              Essai gratuit de 30 jours
+            </v-btn>
           </v-col>
 
           <!-- Partie Illustration -->
@@ -226,7 +236,8 @@ const onIntersect = (isIntersecting: boolean) => {
     margin-bottom: 1rem;
   }
 
-  .learn-more-btn {
+  .learn-more-btn,
+  .artist-premium-btn {
     display: flex;
     align-items: center;
     margin-top: 16px;
@@ -236,7 +247,6 @@ const onIntersect = (isIntersecting: boolean) => {
     font-weight: 700;
     font-size: 0.7rem;
     line-height: 15px;
-    width: 10rem;
     height: 2.5rem;
 
     @media (max-width: 600px) {
@@ -245,6 +255,14 @@ const onIntersect = (isIntersecting: boolean) => {
     }
   }
 
+  .learn-more-btn {
+    width: 10rem;
+  }
+
+  .artist-premium-btn {
+    width: 14rem;
+  }
+
   .learn-more-btn :deep(.v-btn__append) {
     color: var(--on-neutral-color);
     margin-left: 0;

+ 22 - 3
components/Home/Solution.vue

@@ -33,7 +33,7 @@ Section "Solutions" de la page d'accueil
               <v-row>
                 <div :class="['image-container', solution.class]">
                   <v-img :src="solution.image" />
-                  <v-btn v-if="xlAndUp">Découvrir</v-btn>
+                  <v-btn v-if="xlAndUp" class="discover-btn">Découvrir</v-btn>
                 </div>
               </v-row>
             </nuxt-link>
@@ -60,7 +60,17 @@ Section "Solutions" de la page d'accueil
             </v-row>
 
             <v-row v-if="lgAndDown">
-              <v-btn :to="solution.link">Découvrir</v-btn>
+              <v-btn :to="solution.link" class="discover-btn">Découvrir</v-btn>
+            </v-row>
+
+            <v-row>
+              <v-btn
+                v-if="solution.name === 'Artist'"
+                to="shop/try/artist-premium"
+                class="artist-premium-trial-btn"
+              >
+                Essai gratuit de 30 jours
+              </v-btn>
             </v-row>
           </div>
         </v-container>
@@ -248,7 +258,7 @@ h4 {
     transition: opacity 0.3s;
   }
 
-  .v-btn {
+  .discover-btn {
     left: 50%;
     font-size: 0.8rem;
     border-radius: 6px;
@@ -345,6 +355,15 @@ h4 {
   }
 }
 
+.artist-premium-trial-btn {
+  height: 36px;
+  width: 100%;
+  max-width: 350px;
+  background: transparent;
+  color: var(--on-primary-color);
+  border: solid 2px var(--on-primary-color);
+}
+
 .footer {
   p {
     text-align: right;

+ 135 - 0
components/Logiciels/Artist/Trial.vue

@@ -0,0 +1,135 @@
+<template>
+  <AnchoredSection id="trial" class="alt-theme main">
+    <LayoutContainer class="mb-12">
+      <div class="center-90">
+        <v-row class="subtitle">
+          <v-col cols="12">
+            <LayoutUISubTitle class="mt-12 ml-3">
+              Vous souhaitez découvrir le logiciel Opentalen Artist Premium ?
+            </LayoutUISubTitle>
+          </v-col>
+        </v-row>
+      </div>
+    </LayoutContainer>
+
+    <v-row>
+      <v-col cols="12" lg="6" class="content">
+        <h3 class="my-6">
+          Essayez Opentalent Artist Premium gratuitement pendant 30 jours
+        </h3>
+
+        <v-btn
+          to="shop/try/artist-premium"
+          height="48"
+          aria-label="Essayez Opentalent Artist Premium gratuitement pendant 30 jours"
+          class="inv-theme"
+        >
+          Commencer l'essai gratuit
+        </v-btn>
+      </v-col>
+
+      <v-col cols="12" lg="6" class="badges">
+        <div>
+          <v-img
+            src="/images/pages/opentalent_artist/trial/credit_card_off.svg"
+            alt="Icône représentant une carte de crédit barrée"
+          />
+          <p>Aucune carte bleue requise</p>
+        </div>
+
+        <div>
+          <v-img
+            src="/images/pages/opentalent_artist/trial/handshake.svg"
+            alt="Icône représentant une poignée de mains"
+          />
+          <p class="text-btn">Essai sans engagement</p>
+        </div>
+      </v-col>
+    </v-row>
+  </AnchoredSection>
+</template>
+
+<script setup lang="ts">
+import AnchoredSection from '~/components/Layout/AnchoredSection.vue'
+</script>
+
+<style scoped lang="scss">
+.main {
+  min-height: 560px;
+  background-image: url('/images/pages/opentalent_artist/trial/Boutique_en_ligne-Opentalent_Visuel_site.png');
+  background-size: cover;
+  background-position: center 15%;
+  background-repeat: no-repeat;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 1;
+  }
+}
+
+.v-container,
+.v-row {
+  z-index: 2;
+}
+
+h3 {
+  font-size: 28px;
+}
+
+.content {
+  @media (max-width: 1280px) {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    text-align: center;
+    max-width: 90%;
+    margin: 0 auto;
+
+    h3 {
+      text-align: center;
+    }
+  }
+}
+
+.badges {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: center;
+
+  > div {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-start;
+    height: 84px;
+    width: 360px;
+    margin: 12px 0;
+    border: solid 1px var(--on-alt-theme);
+    border-radius: 6px;
+    color: var(--on-alt-theme);
+    font-weight: 700;
+    text-transform: uppercase;
+
+    .v-img {
+      height: 32px;
+      width: 32px;
+      max-width: 32px;
+      margin: 0 24px;
+    }
+  }
+}
+</style>

+ 63 - 0
composables/data/useAp2iRequestService.ts

@@ -0,0 +1,63 @@
+import type { Ref } from 'vue'
+import type { FetchContext, FetchOptions } from 'ofetch'
+import ApiRequestService from '~/services/data/apiRequestService'
+
+/**
+ * Retourne une instance de ApiRequestService configurée pour interroger l'api Ap2i
+ *
+ * @see https://github.com/unjs/ohmyfetch/blob/main/README.md#%EF%B8%8F-create-fetch-with-default-options
+ */
+let apiRequestServiceClass: null | ApiRequestService = null
+
+export const useAp2iRequestService = (retry: number | false = 1) => {
+  const runtimeConfig = useRuntimeConfig()
+
+  const baseURL = runtimeConfig.ap2iBaseUrl ?? runtimeConfig.public.ap2iBaseUrl
+
+  const pending: Ref<boolean> = ref(false)
+
+  /**
+   * Peuple les headers avant l'envoi de la requête
+   *
+   * @param request
+   * @param options
+   */
+  const onRequest = function ({ request, options }: FetchContext) {
+    // @ts-ignore
+    if (options && options.noXaccessId) {
+      return
+    }
+
+    pending.value = true
+    console.log('Request : ' + request + ' (SSR: ' + process.server + ')')
+  }
+
+  const onResponse = function (_: FetchContext) {
+    pending.value = false
+  }
+
+  /**
+   * Gère les erreurs retournées par l'api
+   *
+   * @param request
+   * @param response
+   * @param error
+   */
+
+  const config: FetchOptions = {
+    baseURL,
+    onRequest,
+    onResponse,
+    retry,
+  }
+
+  // Avoid memory leak
+  if (apiRequestServiceClass === null) {
+    // Utilise la fonction `create` d'ohmyfetch pour générer un fetcher dédié à l'interrogation de Ap2i
+    const fetcher = $fetch.create(config)
+    // @ts-ignore
+    apiRequestServiceClass = new ApiRequestService(fetcher)
+  }
+
+  return { ap2iRequestService: apiRequestServiceClass, pending }
+}

+ 78 - 0
composables/utils/useAp2iErrorHandler.ts

@@ -0,0 +1,78 @@
+import { useNuxtApp } from '#app'
+
+/**
+ * Composable for handling API errors, especially for organization-related errors
+ */
+export const useAp2iErrorHandler = () => {
+  const { $i18n } = useNuxtApp()
+
+  /**
+   * Process API error and extract meaningful error message
+   * @param error - The error object from API response
+   * @returns Processed error message
+   */
+  const processApiError = (error: unknown): string => {
+    const defaultErrorMessage =
+      "Une erreur s'est produite. Veuillez réessayer plus tard ou nous contacter directement."
+
+    // Try to extract the specific error message from the API response
+    if (
+      error &&
+      typeof error === 'object' &&
+      'data' in error &&
+      error.data &&
+      typeof error.data === 'object'
+    ) {
+      const errorData = error.data as { detail?: string }
+      if (errorData.detail) {
+        // Check if it's the specific error about organization already existing
+        const organizationExistsRegex =
+          /An organization named '(.+)' already exists in (.+)/
+        const match = errorData.detail.match(organizationExistsRegex)
+
+        if (match) {
+          // Extract the organization name and city name and use the translation
+          const organizationName = match[1]
+          const cityName = match[2]
+          const translationKey =
+            "An organization named '{0}' already exists in {1}"
+
+          const translatedMessage = $i18n.t(translationKey, [
+            organizationName,
+            cityName,
+          ])
+          console.log(translatedMessage, organizationName, cityName)
+
+          // Return only the translated message
+          return translatedMessage as string
+        } else {
+          // Remove the "Handling ... failed:" part if present
+          let cleanedDetail = errorData.detail
+          const handlingFailedRegex = /Handling ".*" failed: (.*)/
+          const handlingMatch = cleanedDetail.match(handlingFailedRegex)
+
+          if (handlingMatch) {
+            cleanedDetail = handlingMatch[1]
+          }
+
+          // Check if a translation exists for this error
+          const translatedMessage = $i18n.t(cleanedDetail)
+
+          // If we have a valid translation (not the same as the key), return only the translation
+          if (translatedMessage !== cleanedDetail) {
+            return translatedMessage as string
+          }
+
+          // Otherwise return the original error message
+          return cleanedDetail
+        }
+      }
+    }
+
+    return defaultErrorMessage
+  }
+
+  return {
+    processApiError,
+  }
+}

+ 4 - 1
env/.env.ci

@@ -1,14 +1,17 @@
 ## LOCAL ENVIRONMENT FILE
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
 NUXT_API_BASE_URL=none
 NUXT_PUBLIC_API_BASE_URL=none
 
+NUXT_AP2I_BASE_URL=none
+NUXT_PUBLIC_AP2I_BASE_URL=none
+
 NUXT_SITE_URL=none
 NUXT_PUBLIC_SITE_URL=none
 
 NUXT_AGENDA_BASE_URL=none
 NUXT_PUBLIC_AGENDA_BASE_URL=none
-

+ 4 - 0
env/.env.docker

@@ -1,11 +1,15 @@
 ## LOCAL ENVIRONMENT FILE
 NUXT_ENV=dev
+NUXT_PUBLIC_ENV=dev
 NUXT_DEBUG=1
 DEBUG=1
 
 NUXT_API_BASE_URL=http://nginx_maestro
 NUXT_PUBLIC_API_BASE_URL=https://local.maestro.opentalent.fr
 
+NUXT_AP2I_BASE_URL=http://nginx_ap2i
+NUXT_PUBLIC_AP2I_BASE_URL=https://local.ap2i.opentalent.fr
+
 NUXT_SITE_URL=http://local.logiciels.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://local.logiciels.opentalent.fr
 

+ 4 - 0
env/.env.prod

@@ -1,6 +1,7 @@
 ## PROD ENVIRONMENT FILE
 # /!\ -- USE ONLY IN PRODUCTION --
 NUXT_ENV=production
+NUXT_PUBLIC_ENV=production
 NUXT_DEBUG=0
 
 PORT=3003
@@ -8,6 +9,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.opentalent.fr
 

+ 6 - 2
env/.env.test

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test1

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test1.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test1.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test1.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test1.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test1.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test1.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test1.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test1.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test1.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test1.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test1.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test1.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test2

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test2.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test2.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test2.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test2.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test2.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test2.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test2.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test2.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test2.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test2.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test2.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test2.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test3

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test3.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test3.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test3.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test3.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test3.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test3.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test3.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test3.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test3.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test3.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test3.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test3.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test4

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test4.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test4.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test4.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test4.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test4.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test4.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test4.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test4.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test4.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test4.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test4.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test4.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test5

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test5.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test5.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test5.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test5.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test5.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test5.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test5.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test5.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test5.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test5.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test5.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test5.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test6

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test6.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test6.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test6.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test6.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test6.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test6.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test6.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test6.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test6.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test6.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test6.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test6.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test7

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test7.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test7.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test7.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test7.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test7.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test7.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test7.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test7.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test7.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test7.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test7.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test7.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test8

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test8.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test8.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test8.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test8.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test8.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test8.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test8.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test8.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test8.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test8.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test8.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test8.opentalent.fr/uploads/share

+ 6 - 2
env/.env.test9

@@ -1,4 +1,5 @@
 NUXT_ENV=test
+NUXT_PUBLIC_ENV=test
 NUXT_DEBUG=1
 DEBUG=1
 
@@ -7,11 +8,14 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test9.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test9.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test9.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test9.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test9.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test9.opentalent.fr
 
-NUXT_AGENDA_BASE_URL=https://agenda.test9.opentalent.fr
-NUXT_PUBLIC_AGENDA_BASE_URL=https://agenda.test9.opentalent.fr
+NUXT_AGENDA_BASE_URL=https://test9.opentalent.fr
+NUXT_PUBLIC_AGENDA_BASE_URL=https://test9.opentalent.fr
 
 NUXT_FILE_STORAGE_BASE_URL=https://maestro.test9.opentalent.fr/uploads/share
 NUXT_PUBLIC_FILE_STORAGE_BASE_URL=https://maestro.test9.opentalent.fr/uploads/share

+ 5 - 1
lang/fr.json

@@ -6,5 +6,9 @@
   "REPLACEMENT": "Remplacement",
   "TEMPORARY": "Vacataire",
   "INTERIM": "Intermittent",
-  "OTHER": "Autre"
+  "OTHER": "Autre",
+  "Invalid phone number": "Numéro de téléphone invalide",
+  "Request expired: submission date is more than 15 minutes old": "Requête expirée : la date de soumission date de plus de 15 minutes",
+  "An organization named '{0}' already exists in {1}": "Une organisation nommée '{0}' existe déjà à {1}",
+  "Invalid request status": "Statut de la requête invalide"
 }

+ 2 - 0
nuxt.config.ts

@@ -41,6 +41,7 @@ export default defineNuxtConfig({
     env: '',
     siteUrl: '',
     apiBaseUrl: '',
+    ap2iBaseUrl: '',
     agendaBaseUrl: '',
     fileStorageBaseUrl: '',
     hCaptchaSiteKey: '35360874-ebb1-4748-86e3-9b156d5bfc53',
@@ -49,6 +50,7 @@ export default defineNuxtConfig({
       env: '',
       siteUrl: '',
       apiBaseUrl: '',
+      ap2iBaseUrl: '',
       agendaBaseUrl: '',
       fileStorageBaseUrl: '',
       hCaptchaSiteKey: '35360874-ebb1-4748-86e3-9b156d5bfc53',

+ 1 - 1
package.json

@@ -41,7 +41,7 @@
     "date-fns": "^3.3.1",
     "eslint-import-resolver-typescript": "^3.6.1",
     "leaflet": "^1.9.3",
-    "libphonenumber-js": "^1.10.55",
+    "libphonenumber-js": "^1.12.9",
     "nuxt": "^3.11.2",
     "nuxt-gtag": "^2.0.6",
     "nuxt-lodash": "^2.5.3",

+ 9 - 4
pages/cgv.vue

@@ -69,7 +69,10 @@
             </ul>
           </div>
 
-          <h2>DURÉE DU CONTRAT ET RÉVISION DES SERVICES OPENTALENT SCHOOL ET OPTIONS</h2>
+          <h2>
+            DURÉE DU CONTRAT ET RÉVISION DES SERVICES OPENTALENT SCHOOL ET
+            OPTIONS
+          </h2>
           <p>
             Le CONTRAT est conclu pour une durée initiale de douze (12) mois à
             compter de l’acceptation de l'OFFRE DE SERVICES/DEVIS/COMMANDE. À
@@ -781,9 +784,11 @@
             comme valant renonciation au paiement.
           </p>
           <p>
-            À noter que ces conditions ne s’appliquent pas aux contrats qui concernent
-            le service Opentalent School et ses options. Les prix des services Opentalent School
-            sont soumis à la clause « DURÉE DU CONTRAT ET RÉVISION DE L’ABONNEMENT OPENTALENT SCHOOL ET OPTIONS” du contrat.
+            À noter que ces conditions ne s’appliquent pas aux contrats qui
+            concernent le service Opentalent School et ses options. Les prix des
+            services Opentalent School sont soumis à la clause « DURÉE DU
+            CONTRAT ET RÉVISION DE L’ABONNEMENT OPENTALENT SCHOOL ET OPTIONS” du
+            contrat.
           </p>
 
           <h2>DISPOSITIONS GÉNÉRALES</h2>

+ 10 - 0
pages/opentalent-artist.vue

@@ -35,6 +35,8 @@
 
       <LogicielsArtistComparatif />
 
+      <LogicielsArtistTrial />
+
       <LogicielsArtistAbonnement />
 
       <LogicielsArtistFormations class="mb-12" />
@@ -59,6 +61,7 @@ const runtimeConfig = useRuntimeConfig()
 
 const menus: Array<MenuScroll> = [
   { anchor: 'presentation', label: 'Présentation' },
+  { anchor: 'trial', label: 'Essai 30J' },
   { anchor: 'benefits', label: 'Avantages' },
   { anchor: 'functionalities', label: 'Fonctionnalités' },
   { anchor: 'comparative', label: 'Comparatif' },
@@ -85,6 +88,13 @@ const stickyMenuActions: Array<ActionMenuItem> = [
       '/Brochures/Brochure_Opentalent_Artist.pdf',
     target: '_blank',
   },
+  {
+    type: ActionMenuItemType.FOLLOW_LINK,
+    color: 'secondary',
+    icon: 'fa-solid fa-vial icon',
+    text: 'Essai Gratuit',
+    url: 'shop/try/artist-premium',
+  },
 ]
 </script>
 

+ 907 - 0
pages/shop/try/artist-premium.vue

@@ -0,0 +1,907 @@
+<template>
+  <div class="theme-artist">
+    <CommonMeta
+      title="Essai gratuit Opentalent Artist Premium - 30 jours sans engagement"
+      description="Essayez gratuitement Opentalent Artist Premium pendant 30 jours. Solution complète pour orchestres, chorales, compagnies de théâtre, de danse ou de cirque."
+    />
+
+    <div class="background-container">
+      <LayoutContainer class="trial-container">
+        <div id="anchor" />
+
+        <v-card class="form-card">
+          <v-card-text>
+            <h1 class="text-center mb-6">
+              Essayez gratuitement Opentalent Artist Premium pendant 30 jours !
+            </h1>
+
+            <div v-if="!trialRequestSent">
+              <div class="description mb-8">
+                <p>
+                  Opentalent Artist Premium est une solution en ligne complète,
+                  pensée pour les orchestres, chorales, compagnies de théâtre,
+                  de danse ou de cirque. Elle vous aide à gagner du temps dans
+                  l'organisation de vos activités, à mieux collaborer avec vos
+                  équipes et à renforcer votre visibilité auprès de votre
+                  public.
+                </p>
+
+                <p>
+                  Pendant 30 jours, profitez de toutes les fonctionnalités
+                  d'Opentalent Artist Premium, gratuitement et sans engagement :
+                </p>
+
+                <ul class="benefits-list">
+                  <li>
+                    <span class="mr-1">✔️</span> Gestion intuitive des membres
+                    et des événements
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Planification avancée des
+                    répétitions, spectacles et tournées
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Outils de communication
+                    intégrés (emails, publipostage, etc.)
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Site web personnalisable pour
+                    présenter vos projets et votre structure
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Accès collaboratif pour vos
+                    équipes, en temps réel
+                  </li>
+                </ul>
+
+                <p>
+                  Il vous suffit de remplir le formulaire ci-dessous pour
+                  activer votre essai gratuit.
+                </p>
+
+                <p>
+                  Lancez-vous dès aujourd'hui et découvrez comment Opentalent
+                  peut transformer votre organisation artistique !
+                </p>
+              </div>
+
+              <v-alert
+                type="info"
+                variant="tonal"
+                border="start"
+                class="mb-4"
+                density="comfortable"
+              >
+                <template #title>
+                  Vous êtes adhérents à la Confédération Musicale de France ?
+                  <br />Et si on vous disait que vous l'aviez déjà...
+                </template>
+                Dans le cadre de votre adhésion, vous bénéficiez de la version
+                Opentalent Artist Standard, et de conditions privilégiées pour
+                la version Artist Premium. Contactez votre fédération pour
+                obtenir vos codes d'accès.
+                <div class="mt-2">
+                  <a href="https://www.cmf-musique.org/contact/" target="_blank"
+                    >Je souhaite obtenir mon code d'accès</a
+                  >
+                </div>
+              </v-alert>
+
+              <v-form
+                ref="form"
+                validate-on="submit lazy"
+                @submit.prevent="submit"
+              >
+                <v-container>
+                  <div v-if="isDevelopmentOrTest" class="dev-tools-container">
+                    <v-btn
+                      color="info"
+                      size="small"
+                      prepend-icon="fa fa-magic"
+                      @click="fillWithDummyData"
+                    >
+                      Remplir avec des données de test
+                    </v-btn>
+                  </div>
+                  <i
+                    >Les champs dont le nom est suivi d'un astérisque (*) sont
+                    obligatoires.</i
+                  >
+
+                  <h2 class="section-title">Coordonnées de la structure</h2>
+
+                  <!-- Structure name -->
+                  <v-row>
+                    <v-col cols="12">
+                      <v-text-field
+                        v-model="trialRequest.structureName"
+                        :rules="[validateRequired]"
+                        label="Nom de la structure*"
+                        required
+                        @input="onStructureNameUpdated"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure address -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.address"
+                        :rules="[validateRequired]"
+                        label="Adresse du siège social de la structure*"
+                        required
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.addressComplement"
+                        label="Adresse (suite)"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.postalCode"
+                        :rules="[validateRequired, validatePostalCode]"
+                        label="Code postal*"
+                        required
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.city"
+                        :rules="[validateRequired]"
+                        label="Ville*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure email and SIREN -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.structureEmail"
+                        :rules="[validateRequired, validateEmail]"
+                        label="Adresse email de la structure*"
+                        required
+                        type="email"
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.siren"
+                        :rules="[validateSiren]"
+                        label="SIREN (optionnel)"
+                        hint="Numéro à 9 chiffres"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure type and legal status -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-select
+                        v-model="trialRequest.structureType"
+                        :rules="[validateRequired]"
+                        label="Type de la structure*"
+                        :items="structureTypes"
+                        item-value="value"
+                        item-title="title"
+                        required
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-select
+                        v-model="trialRequest.legalStatus"
+                        :rules="[validateRequired]"
+                        label="Statut juridique*"
+                        :items="legalStatuses"
+                        item-value="value"
+                        item-title="title"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title">Représentée par</h2>
+
+                  <!-- Representative function -->
+                  <v-row>
+                    <v-col cols="12">
+                      <v-text-field
+                        v-model="trialRequest.representativeFunction"
+                        :rules="[validateRequired]"
+                        label="Fonction*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Representative name -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeFirstName"
+                        :rules="[validateRequired]"
+                        label="Prénom*"
+                        required
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeLastName"
+                        :rules="[validateRequired]"
+                        label="Nom*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Representative contact -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeEmail"
+                        :rules="[validateRequired, validateEmail]"
+                        label="Adresse email*"
+                        required
+                        type="email"
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <CommonPhoneInput
+                        ref="phoneInput"
+                        v-model="trialRequest.representativePhone"
+                        label="Téléphone*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title mb-6">Informations de connexion</h2>
+
+                  <!-- Structure identifier -->
+                  <v-row>
+                    <v-col cols="12" md="6" class="mx-auto">
+                      <v-text-field
+                        v-model="trialRequest.structureIdentifier"
+                        :rules="[
+                          validateRequired,
+                          validateSubdomain,
+                          validateSubdomainAvailability,
+                        ]"
+                        label="Identifiant de la structure*"
+                        required
+                        class="text-center"
+                        @input="onStructureIdentifierUpdated"
+                      />
+                      <div class="validationMessage">
+                        <span v-if="validationPending">
+                          <v-progress-circular size="16" indeterminate />
+                          <i class="ml-2">Vérification en cours</i>
+                        </span>
+                        <span
+                          v-else-if="subdomainAvailable === true"
+                          class="text-success"
+                        >
+                          <v-icon>fa fa-check</v-icon>
+                          <i class="ml-2"> Cet identifiant est disponible</i>
+                        </span>
+                        <span
+                          v-else-if="subdomainAvailable === false"
+                          class="text-error"
+                        >
+                          <v-icon>fa fa-x</v-icon>
+                          <i class="ml-2"
+                            >Cet identifiant n'est pas disponible</i
+                          >
+                        </span>
+                      </div>
+                      <div class="mt-2">
+                        <i v-if="trialRequest.structureIdentifier">
+                          Le compte administrateur de la structure sera
+                          <strong>
+                            admin{{ trialRequest.structureIdentifier }}
+                          </strong>
+                        </i>
+                      </div>
+                      <div>
+                        <i>
+                          Veuillez renseigner un mot de passe pour ce compte :
+                        </i>
+                      </div>
+                    </v-col>
+                  </v-row>
+
+                  <!-- Password field -->
+                  <v-row>
+                    <v-col cols="12" md="6" class="mx-auto">
+                      <v-text-field
+                        v-model="trialRequest.password"
+                        :rules="[validateRequired, validatePassword]"
+                        label="Mot de passe*"
+                        required
+                        :type="showPassword ? 'text' : 'password'"
+                        :append-inner-icon="
+                          showPassword ? 'fa fa-eye-slash' : 'fa fa-eye'
+                        "
+                        @click:append-inner="showPassword = !showPassword"
+                      />
+                      <div class="mt-1">
+                        <i>
+                          Le mot de passe doit contenir au moins 8 caractères,
+                          une minuscule, une majuscule, un chiffre et un
+                          caractère spécial.
+                        </i>
+                      </div>
+                      <v-text-field
+                        v-model="trialRequest.confirmPassword"
+                        :rules="[
+                          showPassword ? () => true : validateRequired,
+                          showPassword ? () => true : validatePasswordMatch,
+                        ]"
+                        label="Confirmer le mot de passe*"
+                        :required="!showPassword"
+                        :disabled="showPassword"
+                        :type="showPassword ? 'text' : 'password'"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title">Accord de termes et conditions</h2>
+
+                  <!-- Terms checkboxes -->
+                  <v-checkbox
+                    v-model="trialRequest.termsAccepted"
+                    :rules="[validateCheckbox]"
+                    required
+                  >
+                    <template #label>
+                      Mon organisme accepte les &nbsp;
+                      <a
+                        href="https://maestro.opentalent.fr/uploads/share/Documents_juridique/CGU.pdf"
+                        target="_blank"
+                      >
+                        conditions générales d'utilisation </a
+                      >.*
+                    </template>
+                  </v-checkbox>
+
+                  <v-checkbox
+                    v-model="trialRequest.legalRepresentative"
+                    :rules="[validateCheckbox]"
+                    label="J'agis en tant que représentant légal de l'association ou de la structure.*"
+                    required
+                  />
+
+                  <v-checkbox
+                    v-model="trialRequest.newsletterSubscription"
+                    label="J'accepte de recevoir la lettre d'information culturelle afin de découvrir des idées de sorties adaptées à ma région."
+                  />
+
+                  <div class="d-flex flex-row justify-center">
+                    <LayoutCaptcha />
+                  </div>
+
+                  <!-- Submit Button -->
+                  <div class="d-flex flex-row justify-center my-10">
+                    <v-btn
+                      type="submit"
+                      color="secondary"
+                      size="large"
+                      class="submit-btn"
+                    >
+                      COMMENCER MON ESSAI DE 30 JOURS
+                    </v-btn>
+                  </div>
+
+                  <p class="text-center no-credit-card">
+                    Aucune carte de crédit requise. En cliquant sur "Commencer
+                    mon essai de 30 jours", vous acceptez de démarrer votre
+                    période d'essai gratuit.
+                  </p>
+
+                  <div v-if="validationError" class="error">
+                    Des champs du formulaire sont invalides ou manquants.
+                  </div>
+
+                  <div v-if="errorMsg" class="error">
+                    {{ errorMsg }}
+                  </div>
+                </v-container>
+
+                <div class="legal">
+                  Les données recueillies par Opentalent sont utilisées pour le
+                  traitement de votre demande et pour vous informer sur nos
+                  offres. Elles sont destinées aux services Opentalent et à ses
+                  sous-traitants pour l'exécution des contrats. Conformément à
+                  la loi "Informatique et Libertés du 6 Janvier 1978", vous
+                  disposez d'un droit d'accès, de modifications, de
+                  rectification et de suppression des données vous concernant.
+                  Pour toute demande, adressez-vous à : OPENTALENT, 265 rue de
+                  la Grange 74950 SCIONZIER - FRANCE, opentalent.fr s'engage à
+                  la confidentialité et à la protection de vos données."
+                </div>
+              </v-form>
+            </div>
+
+            <div
+              v-else
+              class="confirmation-message d-flex flex-row justify-center"
+            >
+              <v-card>
+                <v-card-title class="text-center">
+                  <v-icon
+                    icon="fas fa-check mr-2"
+                    color="success"
+                    max-height="48"
+                  />
+                  Félicitations !
+                </v-card-title>
+                <v-card-text class="text-center">
+                  <p>
+                    Votre demande d'essai gratuit de 30 jours d'Opentalent
+                    Artist Premium a bien été enregistrée, mais nécessite une
+                    validation de votre part.
+                  </p>
+                  <p>
+                    Vous allez recevoir un email permettant de valider cette
+                    demande, à la suite de quoi votre compte sera créé.
+                    Attention, la durée de validité du lien d'activation est de
+                    60 minutes.
+                  </p>
+                  <p>
+                    Notre équipe reste à votre disposition pour vous accompagner
+                    durant cette période d'essai.
+                  </p>
+                </v-card-text>
+                <v-card-actions class="justify-center">
+                  <v-btn
+                    variant="elevated"
+                    prepend-icon="fas fa-arrow-left"
+                    color="secondary"
+                    to="/opentalent-artist"
+                  >
+                    Retour à la page Opentalent Artist
+                  </v-btn>
+                </v-card-actions>
+              </v-card>
+            </div>
+          </v-card-text>
+        </v-card>
+      </LayoutContainer>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+import type { Ref } from 'vue'
+import { reactive } from 'vue'
+import _ from 'lodash'
+import { useRuntimeConfig, useAsyncData } from '#app'
+import type { TrialRequest } from '~/types/interface'
+import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import { useAp2iErrorHandler } from '~/composables/utils/useAp2iErrorHandler'
+import {
+  convertPhoneNumberToInternationalFormat,
+  slugify,
+} from '~/services/utils/stringUtils'
+
+const router = useRouter()
+
+const form: Ref<HTMLElement | null> = ref(null)
+
+// Structure types and legal statuses
+const structureTypes = Object.values(STRUCTURE_TYPE)
+  .map((item) => ({
+    value: item.key,
+    title: item.label,
+  }))
+  .sort((a, b) => (a.title > b.title ? 1 : -1))
+
+const legalStatuses = Object.values(LEGAL_STATUS)
+  .map((item) => ({
+    value: item.key,
+    title: item.label,
+  }))
+  .sort((a, b) => (a.title > b.title ? 1 : -1))
+
+// Get apiRequestService for subdomain availability check
+const { ap2iRequestService } = useAp2iRequestService()
+const { processApiError } = useAp2iErrorHandler()
+
+// Check if we're in a development environment
+const config = useRuntimeConfig()
+const isDevelopmentOrTest = computed(
+  () => config.public.env === 'dev' || config.public.env === 'test'
+)
+
+// Trial request data
+const trialRequest = reactive<TrialRequest>({
+  structureName: '',
+  address: '',
+  addressComplement: '',
+  postalCode: '',
+  city: '',
+  structureEmail: '',
+  structureType: 'ARTISTIC_PRACTICE_ONLY',
+  legalStatus: 'ASSOCIATION_LAW_1901',
+  structureIdentifier: '',
+  siren: '',
+  representativeFirstName: '',
+  representativeLastName: '',
+  representativeFunction: '',
+  representativeEmail: '',
+  representativePhone: '',
+  password: '',
+  confirmPassword: '',
+  termsAccepted: false,
+  legalRepresentative: false,
+  newsletterSubscription: false,
+  createWebsite: false,
+})
+
+// Function to fill the form with dummy data
+const fillWithDummyData = () => {
+  // Generate a short timestamp (unix timestamp in seconds)
+  const shortTimestamp = Math.floor(Date.now() / 1000).toString()
+
+  trialRequest.structureName = `Compagnie Artistique Test ${shortTimestamp}`
+  trialRequest.address = '123 Rue des Arts'
+  trialRequest.addressComplement = 'Bâtiment B'
+  trialRequest.postalCode = '75001'
+  trialRequest.city = 'Paris'
+  trialRequest.structureEmail = 'contact@compagnie-test.fr'
+  trialRequest.structureType = 'ARTISTIC_PRACTICE_ONLY'
+  trialRequest.legalStatus = 'ASSOCIATION_LAW_1901'
+  trialRequest.structureIdentifier = `compagnie-test-${shortTimestamp}`
+  trialRequest.siren = '123456789'
+  trialRequest.representativeFirstName = 'Jean'
+  trialRequest.representativeLastName = 'Dupont'
+  trialRequest.representativeFunction = 'Directeur Artistique'
+  trialRequest.representativeEmail = 'jean.dupont@compagnie-test.fr'
+  trialRequest.representativePhone = '0612345678'
+  trialRequest.password = 'Test1234!'
+  trialRequest.confirmPassword = 'Test1234!'
+  trialRequest.termsAccepted = true
+  trialRequest.legalRepresentative = true
+  trialRequest.newsletterSubscription = true
+
+  // Trigger subdomain availability check
+  checkSubdomainAvailabilityDebounced()
+}
+
+// Track if structure identifier has been manually modified
+const structureIdentifierModified = ref(false)
+
+// Variables for subdomain validation
+const validationPending = ref(false)
+const subdomainAvailable = ref<boolean | null>(null)
+
+const checkSubdomainAvailability = async (subdomain: string) => {
+  if (!subdomain || validateSubdomain(subdomain) !== true) {
+    subdomainAvailable.value = null
+    return false
+  }
+
+  validationPending.value = true
+  try {
+    const subdomainAvailability = await ap2iRequestService.get(
+      '/api/public/subdomains/is_available',
+      { subdomain }
+    )
+
+    subdomainAvailable.value =
+      subdomainAvailability && subdomainAvailability.available === true
+    validationPending.value = false
+    return subdomainAvailable.value
+  } catch (error) {
+    console.error('Error checking subdomain availability:', error)
+    subdomainAvailable.value = false
+    validationPending.value = false
+    return false
+  }
+}
+
+/**
+ * Version debounced de la fonction checkAvailability
+ * @see https://docs-lodash.com/v4/debounce/
+ */
+const checkSubdomainAvailabilityDebounced: _.DebouncedFunc<() => void> =
+  _.debounce(
+    async () =>
+      await checkSubdomainAvailability(trialRequest.structureIdentifier),
+    600
+  )
+
+// Validation rules
+const validateRequired = (value: string) =>
+  !!value || 'Ce champ est obligatoire'
+
+const validateEmail = (email: string) =>
+  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || 'Adresse email invalide'
+
+const validatePostalCode = (postalCode: string) =>
+  /^\d{5}$/.test(postalCode) || 'Code postal invalide (5 chiffres)'
+
+const validateSiren = (siren: string) =>
+  !siren || /^\d{9}$/.test(siren) || 'SIREN invalide (9 chiffres)'
+
+const validateCheckbox = (value: boolean) =>
+  value || 'Vous devez accepter cette condition'
+
+const validatePassword = (password: string) => {
+  if (!password) return 'Ce champ est obligatoire'
+
+  const minLength = password.length >= 8
+  const hasLowercase = /[a-z]/.test(password)
+  const hasUppercase = /[A-Z]/.test(password)
+  const hasNumber = /[0-9]/.test(password)
+  const hasSpecial = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)
+
+  if (!minLength) {
+    return 'Le mot de passe doit contenir au moins 8 caractères'
+  }
+  if (!hasLowercase) {
+    return 'Le mot de passe doit contenir au moins une lettre minuscule'
+  }
+  if (!hasUppercase) {
+    return 'Le mot de passe doit contenir au moins une lettre majuscule'
+  }
+  if (!hasNumber) {
+    return 'Le mot de passe doit contenir au moins un chiffre'
+  }
+  if (!hasSpecial) {
+    return 'Le mot de passe doit contenir au moins un caractère spécial'
+  }
+
+  return true
+}
+
+const validatePasswordMatch = () => {
+  if (!trialRequest.confirmPassword) return 'Ce champ est obligatoire'
+
+  return (
+    trialRequest.password === trialRequest.confirmPassword ||
+    'Les mots de passe ne correspondent pas'
+  )
+}
+
+const validateSubdomain = (value: string) => {
+  if (!value) return 'Ce champ est obligatoire'
+
+  const regex = /^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/
+
+  return (
+    regex.test(value) ||
+    'Format invalide. Utilisez uniquement des lettres minuscules, des chiffres et des tirets. Doit commencer et finir par une lettre ou un chiffre. Maximum 30 caractères.'
+  )
+}
+
+const validateSubdomainAvailability = (value: string) => {
+  if (!value) return ''
+
+  return (
+    subdomainAvailable.value === true || "Cet identifiant n'est pas disponible"
+  )
+}
+
+// Password visibility toggle
+const showPassword = ref(false)
+
+// Form state
+const trialRequestSent: Ref<boolean> = ref(false)
+const errorMsg: Ref<string | null> = ref(null)
+const validationError: Ref<boolean> = ref(false)
+// Reference to the phone input component
+const phoneInput = ref(null)
+
+// Submit function
+const submit = async (): Promise<void> => {
+  const { valid } = await form.value!.validate()
+
+  if (!valid) {
+    validationError.value = true
+    return
+  }
+
+  validationError.value = false
+
+  // Convert phone number to international format before submission
+  if (phoneInput.value) {
+    trialRequest.representativePhone = convertPhoneNumberToInternationalFormat(
+      trialRequest.representativePhone
+    )
+  }
+
+  try {
+    const { data, error } = await useAsyncData('submit-trial-request', () =>
+      ap2iRequestService.post(
+        '/api/public/shop/new-structure-artist-premium-trial-request',
+        trialRequest
+      )
+    )
+
+    if (error.value) {
+      throw error.value
+    }
+
+    console.log('Trial request submitted successfully:', data.value)
+
+    trialRequestSent.value = true
+    errorMsg.value = null
+
+    // Scroll to top to show confirmation message
+    setTimeout(() => router.push({ path: '', hash: '#anchor' }), 30)
+  } catch (e) {
+    console.error('Error submitting trial request:', e)
+
+    // Process the error message using the common error handler
+    const processedError = processApiError(e)
+
+    // Set the error message directly from the processed error
+    // The processApiError function now returns either a translated message
+    // or a generic error message as appropriate
+    errorMsg.value = processedError
+  }
+}
+
+// Event handler for structureName updates
+const onStructureNameUpdated = (newName: string) => {
+  if (!structureIdentifierModified.value && newName) {
+    trialRequest.structureIdentifier = slugify(
+      trialRequest.structureName
+    ).replace(/[-_]/g, '')
+    checkSubdomainAvailabilityDebounced()
+  }
+}
+
+// Event handler for structureIdentifier updates
+const onStructureIdentifierUpdated = () => {
+  structureIdentifierModified.value = true
+  checkSubdomainAvailabilityDebounced()
+}
+</script>
+
+<style scoped lang="scss">
+.background-container {
+  background-image: url('/images/logos/opentalent/Logo_Opentalent_Griffe.png');
+  background-size: 700px;
+  min-height: 100vh;
+}
+
+.trial-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 2rem;
+}
+
+.form-card {
+  box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
+  max-width: 90%;
+  margin: 0 auto;
+  padding: 2rem;
+}
+
+h1 {
+  font-size: 2.5rem;
+  font-weight: 700;
+  color: var(--primary-color);
+  text-decoration: underline var(--artist-color) 3px solid;
+  margin-bottom: 2rem;
+
+  @media (max-width: 768px) {
+    font-size: 1.8rem;
+  }
+}
+
+.description {
+  font-size: 1.1rem;
+  line-height: 1.6;
+  margin-bottom: 2rem;
+
+  p {
+    margin-bottom: 1rem;
+  }
+}
+
+.benefits-list {
+  list-style: none;
+  padding-left: 1rem;
+  margin: 1.5rem 0;
+
+  li {
+    margin-bottom: 0.5rem;
+  }
+}
+
+.section-title {
+  margin-top: 2rem;
+  font-size: 1.5rem;
+  font-weight: 600;
+  color: var(--primary-color);
+  text-decoration: underline var(--artist-color) 3px solid;
+  margin-bottom: 1rem;
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+}
+
+.v-form {
+  max-width: 1200px;
+  margin: 0 auto;
+
+  .submit-btn {
+    font-weight: 600;
+    letter-spacing: 0.05em;
+    padding: 0 2rem;
+  }
+
+  .error {
+    color: var(--warning-color);
+    width: 80%;
+    margin: 0.7em auto 2em;
+    text-align: center;
+    font-size: 1.05rem;
+    font-weight: 600;
+    border: 2px solid var(--warning-color);
+    border-radius: 4px;
+    padding: 0.5rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .legal {
+    opacity: 0.7;
+    font-size: 14px;
+    font-style: italic;
+    margin: 2rem auto;
+    max-width: 80%;
+  }
+}
+
+.no-credit-card {
+  font-size: 0.9rem;
+  opacity: 0.8;
+  margin-top: 1rem;
+}
+
+.confirmation-message {
+  .v-card {
+    max-width: 800px;
+    padding: 2rem;
+    margin: 4rem 0;
+
+    .v-card-title {
+      font-size: 1.8rem;
+      font-weight: 700;
+      color: var(--primary-color);
+    }
+
+    .v-card-text {
+      font-size: 1.1rem;
+      line-height: 1.6;
+
+      p {
+        margin-bottom: 1rem;
+      }
+    }
+  }
+}
+
+.dev-tools-container {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  z-index: 10;
+}
+</style>

+ 145 - 0
pages/shop/try/validation.vue

@@ -0,0 +1,145 @@
+<template>
+  <v-card>
+    <div v-if="loading" class="text-center pa-8">
+      <v-progress-circular indeterminate color="primary" />
+      <p class="mt-4">Validation en cours...</p>
+    </div>
+
+    <div v-else-if="validationSuccess">
+      <v-card-title class="text-center">
+        <v-icon icon="fas fa-check mr-2" color="success" max-height="48" />
+        Félicitations !
+      </v-card-title>
+      <v-card-text class="text-center">
+        <p>Merci d'avoir validé votre demande.</p>
+        <p>Votre compte est en cours de création.</p>
+        <p>
+          Vous recevrez un email dès que votre compte sera prêt à être utilisé.
+        </p>
+      </v-card-text>
+      <v-card-actions class="justify-center">
+        <v-btn
+          variant="elevated"
+          prepend-icon="fas fa-arrow-left"
+          color="secondary"
+          to="/opentalent-artist"
+        >
+          Retour à la page Opentalent Artist
+        </v-btn>
+      </v-card-actions>
+    </div>
+
+    <div v-else class="text-center pa-8">
+      <v-card-title class="text-center">
+        <v-icon
+          icon="fas fa-exclamation-triangle mr-2"
+          color="warning"
+          max-height="48"
+        />
+        Erreur de validation
+      </v-card-title>
+      <v-card-text class="text-center">
+        <p class="error-message">
+          {{ errorMsg }}
+        </p>
+        <p class="mt-4">
+          Si le problème persiste, n'hésitez pas à nous contacter.
+        </p>
+      </v-card-text>
+      <v-card-actions class="justify-center">
+        <v-btn color="primary" to="/nous-contacter">Nous contacter</v-btn>
+      </v-card-actions>
+    </div>
+  </v-card>
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import { useAp2iErrorHandler } from '~/composables/utils/useAp2iErrorHandler'
+import UrlUtils from '~/services/utils/urlUtils'
+
+const route = useRoute()
+const { ap2iRequestService } = useAp2iRequestService(false)
+const runtimeConfig = useRuntimeConfig()
+const { processApiError } = useAp2iErrorHandler()
+
+const loading = ref(true)
+const validationSuccess: Ref<boolean | null> = ref(null)
+const errorMsg: Ref<string | null> = ref(null)
+
+// UUID validation regex
+const uuidRegex =
+  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+
+// Get token from query parameters
+const token = computed(() => {
+  const queryToken = route.query.token
+  return typeof queryToken === 'string' ? queryToken : ''
+})
+
+const validate = async () => {
+  try {
+    // Check if token is present
+    if (!token.value) {
+      errorMsg.value = "Aucun jeton de validation n'a été fourni."
+      return
+    }
+
+    // Validate token format (UUID)
+    if (!uuidRegex.test(token.value)) {
+      errorMsg.value = 'Le format du jeton de validation est invalide.'
+      return
+    }
+
+    // Make API request to validate token
+    await ap2iRequestService.get(
+      UrlUtils.join(
+        runtimeConfig.public.ap2iBaseUrl,
+        '/api/public/shop/validate/',
+        token.value
+      )
+    )
+
+    // If we get here, the validation was successful
+    validationSuccess.value = true
+  } catch (error) {
+    console.error('Error validating token:', error)
+    validationSuccess.value = false
+
+    errorMsg.value = processApiError(error)
+  }
+}
+
+onMounted(async () => {
+  await validate()
+  loading.value = false
+})
+</script>
+
+<style scoped lang="scss">
+.v-card {
+  margin: 4rem 20%;
+  padding: 2rem;
+}
+
+.v-card-title {
+  font-size: 1.8rem;
+  font-weight: 700;
+  color: var(--primary-color);
+}
+
+.v-card-text {
+  font-size: 1.1rem;
+  line-height: 1.6;
+
+  p {
+    margin-bottom: 1rem;
+  }
+}
+
+.error-message {
+  color: var(--warning-color);
+  font-weight: 500;
+}
+</style>

BIN
public/images/pages/opentalent_artist/trial/Boutique_en_ligne-Opentalent_Visuel_site.png


+ 1 - 0
public/images/pages/opentalent_artist/trial/credit_card_off.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="m871-203-71-71v-206H594L434-640h366v-80H354l-80-80h526q33 0 56.5 23.5T880-720v480q0 10-2 19.5t-7 17.5ZM385-462Zm192-35Zm-211 17H160v240h446L366-480ZM818-28 686-160H160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800l80 80h-80v80h46L26-820l57-57L875-85l-57 57Z"/></svg>

+ 1 - 0
public/images/pages/opentalent_artist/trial/handshake.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M475-160q4 0 8-2t6-4l328-328q12-12 17.5-27t5.5-30q0-16-5.5-30.5T817-607L647-777q-11-12-25.5-17.5T591-800q-15 0-30 5.5T534-777l-11 11 74 75q15 14 22 32t7 38q0 42-28.5 70.5T527-522q-20 0-38.5-7T456-550l-75-74-175 175q-3 3-4.5 6.5T200-435q0 8 6 14.5t14 6.5q4 0 8-2t6-4l136-136 56 56-135 136q-3 3-4.5 6.5T285-350q0 8 6 14t14 6q4 0 8-2t6-4l136-135 56 56-135 136q-3 2-4.5 6t-1.5 8q0 8 6 14t14 6q4 0 7.5-1.5t6.5-4.5l136-135 56 56-136 136q-3 3-4.5 6.5T454-180q0 8 6.5 14t14.5 6Zm-1 80q-37 0-65.5-24.5T375-166q-34-5-57-28t-28-57q-34-5-56.5-28.5T206-336q-38-5-62-33t-24-66q0-20 7.5-38.5T149-506l232-231 131 131q2 3 6 4.5t8 1.5q9 0 15-5.5t6-14.5q0-4-1.5-8t-4.5-6L398-777q-11-12-25.5-17.5T342-800q-15 0-30 5.5T285-777L144-635q-9 9-15 21t-8 24q-2 12 0 24.5t8 23.5l-58 58q-17-23-25-50.5T40-590q2-28 14-54.5T87-692l141-141q24-23 53.5-35t60.5-12q31 0 60.5 12t52.5 35l11 11 11-11q24-23 53.5-35t60.5-12q31 0 60.5 12t52.5 35l169 169q23 23 35 53t12 61q0 31-12 60.5T873-437L545-110q-14 14-32.5 22T474-80Zm-99-560Z"/></svg>

+ 19 - 2
services/data/apiRequestService.ts

@@ -72,22 +72,39 @@ class ApiRequestService {
    * @param url
    * @param body
    * @param query
+   * @param headers
    * @protected
    */
   protected async request(
     method: HTTP_METHOD,
     url: string,
     body: string | AnyJson | null = null,
-    query: AssociativeArray | null = null
+    query: AssociativeArray | null = null,
+    headers: AssociativeArray | null = null
   ): Promise<Response> {
     const config: FetchOptions = { method }
     if (query) {
       config.query = query
     }
-    if (method === HTTP_METHOD.POST || method === HTTP_METHOD.PUT) {
+
+    config.headers = headers ? { ...headers } : {}
+
+    config.headers.Accept = 'application/ld+json'
+    config.headers['Content-Type'] = 'application/ld+json'
+
+    if (
+      method === HTTP_METHOD.POST ||
+      method === HTTP_METHOD.PUT ||
+      method === HTTP_METHOD.PATCH
+    ) {
       config.body = body
     }
 
+    if (method === HTTP_METHOD.PATCH) {
+      // config.headers['Accept'] = 'application/merge-patch+json'
+      config.headers['Content-Type'] = 'application/merge-patch+json'
+    }
+
     // @ts-expect-error TODO: solve the type mismatch
     return await this.fetch(url, config)
   }

+ 44 - 0
services/utils/stringUtils.ts

@@ -0,0 +1,44 @@
+/**
+ * String utility functions
+ */
+import { type CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js'
+
+/**
+ * Convert a string to a slug format
+ * Removes special characters, replaces spaces with hyphens, and converts to lowercase
+ *
+ * @param text The text to convert to slug format
+ * @returns The slugified text
+ */
+export function slugify(text: string): string {
+  return text
+    .toString()
+    .normalize('NFD') // Split accented characters
+    .replace(/[\u0300-\u036F]/g, '') // Remove diacritics
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-') // Replace spaces with -
+    .replace(/[^\w-]+/g, '') // Remove all non-word chars
+    .replace(/--+/g, '-') // Replace multiple - with single -
+    .replace(/^-+/, '') // Trim - from start of text
+    .replace(/-+$/, '') // Trim - from end of text
+    .substring(0, 30) // Limit to 30 characters
+}
+
+// Function to convert phone number to international format
+export function convertPhoneNumberToInternationalFormat(
+  phone: string,
+  countryCode: CountryCode = 'FR'
+): string {
+  try {
+    // Use the provided country code or default to FR
+    const phoneNumber = parsePhoneNumberWithError(phone, countryCode)
+    if (phoneNumber && phoneNumber.isValid()) {
+      return phoneNumber.format('E.164') // format: +33123456789
+    }
+    return phone
+  } catch (error) {
+    console.error('Error converting phone number:', error)
+    return phone
+  }
+}

+ 1 - 0
types/enum/data.ts

@@ -2,6 +2,7 @@ export const enum HTTP_METHOD {
   POST = 'POST',
   PUT = 'PUT',
   GET = 'GET',
+  PATCH = 'PATCH',
   DELETE = 'DELETE',
 }
 

+ 1 - 0
types/enum/enums.ts

@@ -1,4 +1,5 @@
 export const enum PRODUCT {
+  FREEMIUM = 'freemium',
   SCHOOL = 'school',
   SCHOOL_PREMIUM = 'school-premium',
   ARTIST = 'artist',

+ 25 - 0
types/interface.d.ts

@@ -1,5 +1,6 @@
 import { ActionMenuItemType } from '~/types/enum/layout'
 import { COOKIE_CONSENT_CHOICE } from '~/types/enum/enums'
+import { STRUCTURE_TYPE_KEYS, LEGAL_STATUS_KEYS } from '~/types/types'
 
 interface ActionMenuItem {
   type: ActionMenuItemType
@@ -182,3 +183,27 @@ interface Article {
   btnTitle?: string
   btnHref?: string
 }
+
+interface TrialRequest {
+  structureName: string
+  address: string
+  addressComplement: string
+  postalCode: string
+  city: string
+  structureEmail: string
+  structureType: STRUCTURE_TYPE_KEYS
+  legalStatus: LEGAL_STATUS_KEYS
+  structureIdentifier: string
+  siren: string
+  representativeFirstName: string
+  representativeLastName: string
+  representativeFunction: string
+  representativeEmail: string
+  representativePhone: string
+  password: string
+  confirmPassword: ?string
+  termsAccepted: boolean
+  legalRepresentative: boolean
+  newsletterSubscription: boolean
+  createWebsite: boolean
+}

+ 33 - 0
types/types.ts

@@ -0,0 +1,33 @@
+export const STRUCTURE_TYPE = {
+  ARTISTIC_EDUCATION_ONLY: {
+    key: 'ARTISTIC_EDUCATION_ONLY',
+    label: 'Enseignement artistique uniquement',
+  },
+  ARTISTIC_PRACTICE_ONLY: {
+    key: 'ARTISTIC_PRACTICE_ONLY',
+    label: 'Pratique artistique uniquement',
+  },
+  ARTISTIC_PRACTICE_EDUCATION: {
+    key: 'ARTISTIC_PRACTICE_EDUCATION',
+    label: 'Pratique et enseignement artistique',
+  },
+} as const
+
+export type STRUCTURE_TYPE_KEYS = keyof typeof STRUCTURE_TYPE
+
+export const LEGAL_STATUS = {
+  LOCAL_AUTHORITY: {
+    key: 'LOCAL_AUTHORITY',
+    label: 'Collectivité territoriale',
+  },
+  ASSOCIATION_LAW_1901: {
+    key: 'ASSOCIATION_LAW_1901',
+    label: 'Association loi 1901',
+  },
+  COMMERCIAL_SOCIETY: {
+    key: 'COMMERCIAL_SOCIETY',
+    label: 'Société commerciale',
+  },
+} as const
+
+export type LEGAL_STATUS_KEYS = keyof typeof LEGAL_STATUS

+ 5 - 5
yarn.lock

@@ -8564,10 +8564,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"libphonenumber-js@npm:^1.10.55":
-  version: 1.11.4
-  resolution: "libphonenumber-js@npm:1.11.4"
-  checksum: 10c0/0a606da67b4b465e6e157570ad5e70b92f59197cdc1c505d160422a21a894b55a75c9044b863d0eaf4d96884d3fa3e77268adf55afc2d8f11efae7f7a249e7cc
+"libphonenumber-js@npm:^1.12.9":
+  version: 1.12.9
+  resolution: "libphonenumber-js@npm:1.12.9"
+  checksum: 10c0/77fb86802cdf339f472e6a65394603fec0dabe76faca8b5efb4ebcfada2c11a7b39b6773de400e8ec6528354d4b49317797673b14e921117cadbe6b76f01613d
   languageName: node
   linkType: hard
 
@@ -11446,7 +11446,7 @@ __metadata:
     eslint-plugin-vue: "npm:^9.21.1"
     jsdom: "npm:^24.0.0"
     leaflet: "npm:^1.9.3"
-    libphonenumber-js: "npm:^1.10.55"
+    libphonenumber-js: "npm:^1.12.9"
     nuxt: "npm:^3.11.2"
     nuxt-gtag: "npm:^2.0.6"
     nuxt-lodash: "npm:^2.5.3"