Browse Source

Merge branch 'feature/boutique' into develop

# Conflicts:
#	components/Layout/Header.vue
#	i18n/lang/fr.json
#	nuxt.config.ts
#	services/data/normalizer/hydraNormalizer.ts
Vincent 8 months ago
parent
commit
5cfe419314

+ 10 - 0
assets/css/theme.scss

@@ -51,3 +51,13 @@
   background-color: rgb(var(--v-theme-x-create-btn)) !important;
   color: rgb(var(--v-theme-on-x-create-btn)) !important;
 }
+
+.theme-artist {
+  background-color: rgb(var(--v-theme-artist)) !important;
+  color: rgb(var(--v-theme-on-surface)) !important;
+}
+
+.theme-school {
+  background-color: rgb(var(--v-theme-school)) !important;
+  color: rgb(var(--v-theme-on-primary)) !important;
+}

+ 1 - 1
components/Layout/AlertBar/SuperAdmin.vue

@@ -39,7 +39,7 @@ const url: ComputedRef<string> = computed(() => {
 
   if (show && orgId && originalAccessId) {
     return makeAdminUrl(
-      UrlUtils.join('#', 'switch_user', orgId, originalAccessId, 'exit'),
+      UrlUtils.join('switch_user', orgId, originalAccessId, 'exit'),
     )
   }
   return ''

+ 8 - 0
components/Layout/Dialog.vue

@@ -14,6 +14,7 @@
         "
       >
         <h3 class="d-flex">
+          <v-icon icon="fa-solid fa-bullhorn" />
           <slot name="dialogType" />
         </h3>
       </div>
@@ -83,6 +84,13 @@ const _show = computed(() => props.show) as boolean
     writing-mode: vertical-lr;
     transform: rotate(-180deg);
   }
+
+  .v-icon {
+    font-size: 25px;
+    transform: rotate(90deg);
+    padding-right: 20px;
+    padding-bottom: 10px;
+  }
 }
 
 .dialog-container {

+ 68 - 0
components/Layout/Dialog/Trial/AlreadyDid.vue

@@ -0,0 +1,68 @@
+<template>
+  <LazyLayoutDialog :show="show" theme="warning">
+    <template #dialogType>{{ $t('important') }}</template>
+    <template #dialogTitle>{{ $t('trial_all_ready_did') }}</template>
+    <template #dialogText>
+      <v-card-text class="text">
+        <p>
+          Au cours des 6 derniers mois, vous avez bénéficié d’un essai gratuit
+          de 30 jours du logiciel Opentalent Artist Premium.
+        </p>
+        <p>
+          Pour continuer à explorer toutes les fonctionnalités de notre solution
+          et optimiser la gestion de votre structure, nous vous invitons à
+          souscrire à l’une de nos offres adaptées à vos besoins.
+        </p>
+        <p>
+          Si toutefois vous souhaitez une réactivation exceptionnelle de
+          l’essai, n’hésitez pas à contacter notre équipe Opentalent. 
Nous
+          serons ravis d’évaluer votre demande et de vous accompagner dans vos
+          projets.
+        </p>
+      </v-card-text>
+    </template>
+    <template #dialogBtn>
+      <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="closeDialog">
+        {{ $t('cancel') }}
+      </v-btn>
+      <v-btn class="mr-4 submitBtn theme-warning" @click="contactOpentalent">
+        {{ $t('opentalent_contact') }}
+      </v-btn>
+    </template>
+  </LazyLayoutDialog>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  show: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
+})
+const emit = defineEmits(['closeDialog'])
+
+const closeDialog = () => {
+  emit('closeDialog')
+}
+
+const contactOpentalent = async () => {
+  emit('closeDialog')
+
+  await navigateTo('https://logiciels.opentalent.fr/nous-contacter', {
+    open: {
+      target: '_blank',
+    },
+    external: true,
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.text {
+  font-size: 13px;
+  p {
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 74 - 0
components/Layout/Dialog/Trial/StopConfirmation.vue

@@ -0,0 +1,74 @@
+<template>
+  <LazyLayoutDialog :show="show" theme="danger">
+    <template #dialogType>{{ $t('important') }}</template>
+    <template #dialogTitle
+      >{{ $t('you_want_to_stop_your_premium_trial_period') }} ?
+    </template>
+    <template #dialogText>
+      <v-card-text class="text">
+        <p>
+          {{ $t('stop_trial_period_warning_1a') }}
+          {{
+            $t(
+              organizationProfile.productBeforeTrial ??
+                'stop_trial_missing_version_label',
+            )
+          }}, {{ $t('stop_trial_period_warning_1b') }}
+        </p>
+        <p>
+          <strong>{{ $t('stop_trial_period_warning_2') }}</strong>
+        </p>
+        <ul>
+          <li>{{ $t('stop_trial_period_warning_3') }}</li>
+          <li>{{ $t('stop_trial_period_warning_4') }}</li>
+          <li>{{ $t('stop_trial_period_warning_5') }}</li>
+        </ul>
+        <p>{{ $t('stop_trial_period_warning_6') }}</p>
+      </v-card-text>
+    </template>
+    <template #dialogBtn>
+      <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="closeDialog">
+        {{ $t('cancel') }}
+      </v-btn>
+      <v-btn class="mr-4 submitBtn theme-danger" @click="stopTrial">
+        {{ $t('stop_trial') }}
+      </v-btn>
+    </template>
+  </LazyLayoutDialog>
+</template>
+
+<script setup lang="ts">
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+
+defineProps({
+  show: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
+})
+
+const organizationProfile = useOrganizationProfileStore()
+
+const emit = defineEmits(['closeDialog', 'stopTrial'])
+
+const closeDialog = () => {
+  emit('closeDialog')
+}
+const stopTrial = () => {
+  emit('stopTrial')
+}
+</script>
+
+<style scoped lang="scss">
+.text {
+  p {
+    margin-bottom: 10px;
+  }
+
+  ul {
+    padding-left: 20px;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 1 - 1
components/Layout/Header.vue

@@ -46,7 +46,7 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
     <LayoutHeaderMenu name="Account" color="on-primary" icon="fas fa-sun" />
 
     <a
-      :href="runtimeConfig.public.supportUrl"
+      :href="runtimeConfig.supportUrl || runtimeConfig.public.supportUrl"
       class="text-body px-3 py-4 ml-2 theme-secondary text-decoration-none h-100"
       target="_blank"
     >

+ 36 - 14
components/Layout/MainMenu.vue

@@ -8,10 +8,13 @@ Prend en paramètre une liste de ItemMenu et les met en forme
     v-model="displayMenu"
     :rail="isRail"
     :disable-resize-watcher="true"
+    style="z-index: 1006"
     class="theme-secondary main-menu"
   >
+    <!-- Forcing z-index to avoid this : https://github.com/vuetifyjs/nuxt-module/issues/205 -->
+
     <template #prepend>
-      <slot name="title"></slot>
+      <slot name="prepend" :is-rail="isRail" />
     </template>
 
     <v-list open-strategy="single" active-class="active" class="left-menu">
@@ -20,6 +23,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
         <!-- Cas 1 : l'item n'a pas d'enfants, c'est un lien (ou le menu est en mode réduit) -->
         <v-list-item
           v-if="!item.children || isRail"
+          :id="'main-menu-item' + item.label"
           :title="$t(item.label)"
           :prepend-icon="item.icon.name"
           :href="!isInternalLink(item) ? item.to : undefined"
@@ -39,6 +43,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
           <template #activator="{ props }">
             <v-list-item
               v-bind="props"
+              :id="'main-menu-item' + item.label"
               :prepend-icon="item.icon.name"
               :title="$t(item.label)"
               class="theme-secondary menu-item"
@@ -48,8 +53,9 @@ Prend en paramètre une liste de ItemMenu et les met en forme
 
           <v-list-item
             v-for="child in item.children"
-            :key="$t(child.label)"
+            :key="child.label"
             :title="$t(child.label)"
+            :id="'main-menu-item' + item.label + '-' + child.label"
             :prepend-icon="child.icon.name"
             :href="!isInternalLink(child) ? child.to : undefined"
             :to="isInternalLink(child) ? child.to : undefined"
@@ -62,7 +68,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
     </v-list>
 
     <template #append>
-      <slot name="foot"></slot>
+      <slot name="foot" :is-rail="isRail" />
     </template>
   </v-navigation-drawer>
 </template>
@@ -72,7 +78,10 @@ import { useMenu } from '~/composables/layout/useMenu'
 import { computed } from '@vue/reactivity'
 import { useDisplay } from 'vuetify'
 import type { MenuGroup, MenuItem } from '~/types/layout'
+import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
 
+const i18n = useI18n()
+const organizationProfile = useOrganizationProfileStore()
 const { getMenu, hasMenu, isInternalLink, setMenuState, isMenuOpened } =
   useMenu()
 
@@ -80,22 +89,15 @@ const { mdAndUp, lgAndUp } = useDisplay()
 
 const menu = getMenu('Main')
 
-const isOpened = computed(() => isMenuOpened('Main'))
-
-let items: Array<MenuGroup | MenuItem>
-if (menu === null) {
-  items = []
-} else if (menu.hasOwnProperty('children')) {
-  items = (menu as MenuGroup).children ?? []
-} else {
-  items = [menu]
-}
-
 // En vue lg+, on affiche toujours le menu
 const displayMenu = computed(() => {
   return menu !== null && hasMenu('Main') && (lgAndUp.value || isOpened.value)
 })
 
+const isOpened = computed(() => isMenuOpened('Main'))
+
+const items: Array<MenuGroup | MenuItem> = getItems(menu)
+
 // En vue md+, fermer le menu le passe simplement en mode rail
 // Sinon, le fermer le masque complètement
 const isRail = computed(() => {
@@ -117,6 +119,26 @@ const unwatch = watch(lgAndUp, (newValue, oldValue) => {
 onUnmounted(() => {
   unwatch()
 })
+
+/**
+ * Récupère les menuItem disponibles
+ * @param menu
+ */
+function getItems(
+  menu: MenuGroup | MenuItem | null,
+): Array<MenuGroup | MenuItem> {
+  let items: Array<MenuGroup | MenuItem>
+
+  if (menu === null) {
+    items = []
+  } else if (menu.hasOwnProperty('children')) {
+    items = (menu as MenuGroup).children ?? []
+  } else {
+    items = [menu]
+  }
+
+  return items
+}
 </script>
 
 <style scoped lang="scss">

+ 140 - 0
components/Layout/Pages/Subscription/Card.vue

@@ -0,0 +1,140 @@
+<template>
+  <v-card
+    elevation="2"
+    outlined
+    shaped
+    class="card"
+    :class="{ ['border-' + color]: true }"
+  >
+    <span
+      v-if="extraHeader"
+      class="extraBorder"
+      :class="'extraBorder-' + color"
+      >{{ extraHeader }}</span
+    >
+
+    <div class="card-content">
+      <!-- Titre -->
+      <v-card-title class="title" :class="{ ['margin-sup']: !extraHeader }">
+        {{ title }}
+      </v-card-title>
+
+      <v-card-subtitle class="subtitle">
+        {{ subTitle }}
+        <slot name="card.subTitle" />
+      </v-card-subtitle>
+
+      <!-- Texte -->
+      <v-card-text>
+        <LayoutPagesSubscriptionList :elements="list" :color="color" />
+      </v-card-text>
+
+      <!-- Actions -->
+      <v-card-actions class="mb-3 card-actions">
+        <slot name="card.action" />
+      </v-card-actions>
+    </div>
+  </v-card>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  title: {
+    type: String,
+    required: true,
+  },
+  subTitle: {
+    type: String,
+    required: false,
+  },
+  extraHeader: {
+    type: String,
+    required: false,
+  },
+  color: {
+    type: String,
+    required: true,
+  },
+  list: {
+    type: Array,
+    required: true,
+  },
+})
+</script>
+
+<style scoped lang="scss">
+.card {
+  border-width: 1px;
+  border-top-width: 4px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  position: relative;
+
+  .title {
+    padding-top: 10px;
+    white-space: normal;
+    text-transform: uppercase;
+  }
+  .subtitle {
+    text-transform: uppercase;
+    font-weight: bold;
+    font-size: 1.25rem;
+    opacity: unset;
+  }
+  //1280 1670
+  :deep(.v-btn) {
+    padding: 10px;
+    width: 100%;
+    @media (min-width: 1280px) and (max-width: 1670px) {
+      letter-spacing: 0px;
+      font-size: 11px;
+    }
+  }
+}
+
+.card-content {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  padding: 10px;
+}
+
+.card-actions {
+  margin-top: auto; // pousse les actions en bas
+  display: flex;
+  flex-direction: column;
+}
+
+.extraBorder {
+  text-align: center;
+  text-transform: uppercase;
+  margin: auto;
+  border-radius: 0px 0px 5px 5px;
+  width: 90%;
+  padding: 2px 10px 2px 10px;
+  font-weight: bold;
+  display: block;
+}
+.card.border-primary {
+  border-color: rgb(var(--v-theme-primary));
+}
+.card.border-artist {
+  border-color: rgb(var(--v-theme-artist));
+}
+.card.border-school {
+  border-color: rgb(var(--v-theme-school));
+}
+.extraBorder.extraBorder-artist {
+  background: rgb(var(--v-theme-artist));
+}
+.extraBorder.extraBorder-school {
+  background: rgb(var(--v-theme-school));
+  color: #fff;
+}
+
+.margin-sup {
+  margin-top: 30px;
+}
+</style>

+ 36 - 0
components/Layout/Pages/Subscription/List.vue

@@ -0,0 +1,36 @@
+<template>
+  <ul>
+    <li v-for="li in elements">
+      <v-icon
+        class="check"
+        :color="color"
+        icon="fa-solid fa-check"
+        size="large"
+      ></v-icon>
+      <span class="pl-2">{{ li }}</span>
+    </li>
+  </ul>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  elements: {
+    type: Array,
+    required: true,
+  },
+  color: {
+    type: String,
+    required: true,
+  },
+})
+</script>
+
+<style scoped lang="scss">
+ul {
+  list-style: none;
+  .check {
+    font-size: 15px;
+    font-weight: bold;
+  }
+}
+</style>

+ 1 - 1
components/Layout/Subheader.vue

@@ -93,7 +93,7 @@ const showDateTimeRange: Ref<boolean> = ref(
 
 <style scoped lang="scss">
 main {
-  font-size: 12px;
+  font-size: 13px;
 }
 
 #subheader {

+ 110 - 0
components/Layout/UpgradePremiumButton.vue

@@ -0,0 +1,110 @@
+<template>
+  <div>
+    <div :class="['btn_trial', { minimized }]" @click="trialAction()">
+      <v-icon icon="fa fa-ticket" />
+
+      <span v-if="organizationProfile.isTrialActive && !minimized">
+        <strong>J-{{ organizationProfile.trialCountDown }}</strong>
+        <br />
+      </span>
+
+      <span v-if="!minimized">{{ btnLabel }}</span>
+    </div>
+
+    <LayoutDialogTrialAlreadyDid
+      :show="showDialog"
+      @closeDialog="showDialog = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import UrlUtils from '~/services/utils/urlUtils'
+import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
+import { computed } from '@vue/reactivity'
+
+const runtimeConfig = useRuntimeConfig()
+const organizationProfile = useOrganizationProfileStore()
+const { apiRequestService } = useApiLegacyRequestService()
+const i18n = useI18n()
+
+defineProps({
+  minimized: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
+})
+
+const showDialog: Ref<boolean> = ref(false)
+
+const btnLabel = computed(() => {
+  if (organizationProfile.isTrialActive) {
+    return i18n.t('trial_started')
+  }
+  return organizationProfile.principalType === 'ARTISTIC_PRACTICE_ONLY'
+    ? i18n.t('try_premium')
+    : i18n.t('discover_offer')
+})
+
+/**
+ * Lorsque l'on appuie sur le bouton pour démarrer l'essai / découvrir les offres
+ */
+const trialAction = async () => {
+  const v1BaseURL =
+    runtimeConfig.baseUrlAdminLegacy || runtimeConfig.public.baseUrlAdminLegacy
+
+  if (organizationProfile.isTrialActive) {
+    await navigateTo(UrlUtils.join(v1BaseURL, '#', 'subscribe'), {
+      external: true,
+    })
+  } else if (organizationProfile.principalType === 'ARTISTIC_PRACTICE_ONLY') {
+    try {
+      await apiRequestService.get('/trial/is_available')
+      await navigateTo(UrlUtils.join(v1BaseURL, '#', 'trial'), {
+        external: true,
+      })
+    } catch (error) {
+      showDialog.value = true
+    }
+  } else {
+    await navigateTo('/subscription')
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.btn_trial {
+  background-color: rgb(var(--v-theme-x-create-btn));
+  border-radius: 5px;
+  border: 1px solid #fff;
+  margin-left: 15px;
+  margin-right: 15px;
+  text-align: center;
+  color: #000;
+  margin-top: 5px;
+  padding: 5px 10px;
+  cursor: pointer;
+  white-space: pre-line;
+  font-size: 13px;
+
+  .v-icon {
+    font-size: 16px;
+    color: #000;
+    padding-right: 5px;
+    margin: 0 5px 4px 0;
+  }
+}
+
+.minimized {
+  font-size: 17px;
+  margin-left: 7px;
+  margin-right: 7px;
+  padding: 0;
+
+  .v-icon {
+    padding-right: 0;
+    margin: 0 0 3px 0;
+  }
+}
+</style>

+ 2 - 2
components/Ui/ExpansionPanel.vue

@@ -7,7 +7,7 @@ Panneaux déroulants de type "accordéon"
 <template>
   <v-expansion-panel :value="title">
     <v-expansion-panel-title color="neutral">
-      <template v-slot:default="{ expanded }">
+      <template #default>
         <v-icon class="theme-primary icon">
           {{ icon }}
         </v-icon>
@@ -22,7 +22,7 @@ Panneaux déroulants de type "accordéon"
 </template>
 
 <script setup lang="ts">
-const props = defineProps({
+defineProps({
   title: {
     type: String,
     required: true,

+ 2 - 0
components/Ui/SystemBar.vue

@@ -8,8 +8,10 @@ System bars
     :class="
       'd-flex flex-row justify-center align-center text-center ' + classes
     "
+    style="z-index: 1006"
     @click="onClick !== undefined ? onClick() : null"
   >
+    <!-- Forcing z-index to avoid this : https://github.com/vuetifyjs/nuxt-module/issues/205 -->
     <slot>
       <v-icon v-if="icon" small :icon="icon" />
       {{ text }}

+ 116 - 0
composables/data/useApiLegacyRequestService.ts

@@ -0,0 +1,116 @@
+import type { Ref } from 'vue'
+import type { FetchContext, FetchOptions } from 'ofetch'
+import { TYPE_ALERT } from '~/types/enum/enums'
+import ApiRequestService from '~/services/data/apiRequestService'
+import { usePageStore } from '~/stores/page'
+import UnauthorizedError from '~/services/error/UnauthorizedError'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import UrlUtils from '~/services/utils/urlUtils'
+
+/**
+ * Retourne une instance de ApiRequestService configurée pour interroger l'api legacy
+ *
+ * @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 useApiLegacyRequestService = () => {
+  const runtimeConfig = useRuntimeConfig()
+
+  const baseURL = UrlUtils.join(
+    runtimeConfig.baseUrlLegacy || runtimeConfig.public.baseUrlLegacy,
+    'api',
+  )
+
+  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-expect-error options is not aware of noXaccessId
+    if (options && options.noXaccessId) {
+      return
+    }
+
+    const accessProfileStore = useAccessProfileStore()
+
+    const headers = new Headers(options.headers)
+
+    headers.set('X-Accessid', String(accessProfileStore.id))
+    headers.set('Authorization', 'BEARER ' + accessProfileStore.bearer)
+    if (accessProfileStore.switchId) {
+      headers.set('X-Switch-Access', String(accessProfileStore.switchId))
+    }
+    options.headers = headers
+
+    pending.value = true
+  }
+
+  const onRequestError = function (_: FetchContext) {
+    pending.value = false
+  }
+
+  /**
+   * Server responded
+   */
+  const onResponse = function (_: FetchContext) {
+    pending.value = false
+  }
+
+  /**
+   * Gère les erreurs retournées par l'api
+   *
+   * @param request
+   * @param response
+   * @param error
+   */
+  const onResponseError = function ({ response, error }: FetchContext) {
+    pending.value = false
+
+    if (response && response.status === 401) {
+      throw new UnauthorizedError('Api - Unauthorized')
+    } else if (response && response.status === 403) {
+      console.error('! Request error: Forbidden')
+      usePageStore().addAlert(TYPE_ALERT.ALERT, ['forbidden'])
+    } else if (
+      response &&
+      (response.status === 400 || response.status >= 404)
+    ) {
+      // @see https://developer.mozilla.org/fr/docs/Web/HTTP/Status
+      let errorMsg
+      if (error) {
+        errorMsg = error.message
+      } else if (response._data && response._data.detail) {
+        errorMsg = response._data.detail
+      } else if (response.statusText) {
+        errorMsg = response.statusText
+      } else {
+        errorMsg = 'An error occured'
+      }
+
+      console.error('! Request error: ' + errorMsg)
+      usePageStore().addAlert(TYPE_ALERT.ALERT, [errorMsg])
+    }
+  }
+
+  const config: FetchOptions = {
+    baseURL,
+    onRequest,
+    onRequestError,
+    onResponse,
+    onResponseError,
+  }
+
+  // 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)
+
+    apiRequestServiceClass = new ApiRequestService(fetcher)
+  }
+
+  return { apiRequestService: apiRequestServiceClass, pending }
+}

+ 1 - 1
composables/utils/useDownloadFromRoute.ts

@@ -11,7 +11,7 @@ export const useDownloadFromRoute = async (route: string, filename: string) => {
   const { apiRequestService } = useAp2iRequestService()
 
   // @ts-expect-error La méthode get renvoie bien un blob dans ce cas là
-  const response = await apiRequestService.get(route) as Blob
+  const response = (await apiRequestService.get(route)) as Blob
 
   if (!response || response.size === 0) {
     console.error('Error: no file found at ' + route)

+ 8 - 0
config/theme.ts

@@ -34,6 +34,8 @@ interface Theme {
     'on-warning': string
     info: string
     'on-info': string
+    artist: string
+    school: string
 
     // Special cases
     // TODO: voir ceux dont on peut se passer
@@ -91,6 +93,9 @@ export const lightTheme: Theme = {
 
     'x-create-btn': '#f39c12',
     'on-x-create-btn': '#ffffff',
+
+    artist: '#fac20a',
+    school: '#1893bf',
   },
 }
 
@@ -143,5 +148,8 @@ export const darkTheme: Theme = {
 
     'x-create-btn': '#f39c12',
     'on-x-create-btn': '#ffffff',
+
+    artist: '#fac20a',
+    school: '#1893bf',
   },
 }

+ 29 - 2
i18n/lang/fr.json

@@ -1,4 +1,15 @@
 {
+  "price_include_cmf": "Inclus avec votre adhésion CMF",
+  "artist": "Artist Standard",
+  "trial_started": "Sur votre période d'essai.\nSouscrire à l'offre Premium.",
+  "important": "Important",
+  "trial_all_ready_did": "Vous avez déjà bénéficié d’un essai gratuit !",
+  "opentalent_contact": "Contacter Opentalent",
+  "discover_offer": "Découvrir toutes les offres",
+  "try_premium": "Essayer Opentalent Artist Premium gratuitement pendant 30J",
+  "opentalent_options": "Les options Opentalent",
+  "opentalent_offers": "Les offres Opentalent",
+  "service_detail": "Détail des services",
   "my_settings_page": "Mes paramètres",
   "allow_report_message": "Je souhaite recevoir les rapports d'envoi des emails que j'envoie",
   "my-settings_breadcrumbs": "Mes paramètres",
@@ -117,6 +128,7 @@
   "BIG_BAND": "Big band",
   "PRODUCT_ARTIST": "Opentalent Artist",
   "PRODUCT_ARTIST_PREMIUM": "Opentalent Artist Premium",
+  "PRODUCT_ARTIST_PREMIUM_TRIAL": "Opentalent Artist Premium (Essai)",
   "PRODUCT_SCHOOL": "Opentalent School",
   "PRODUCT_SCHOOL_PREMIUM": "Opentalent School Premium",
   "PRODUCT_MANAGER": "Opentalent Manager",
@@ -529,7 +541,7 @@
   "informations": "Informations",
   "more_features": "Plus de fonctionnalités",
   "client_id": "Numéro de client",
-  "version": "Version",
+  "version": "Version logiciel",
   "services": "Services",
   "bills": "Factures",
   "paid": "Payée",
@@ -708,5 +720,20 @@
   "missing_name": "Nom manquant",
   "warning": "Avertissement",
   "please_enter_a_value_for_the_sms_sender_name": "Le nom d'expediteur ne doit pas comporter plus de 11 caractères, et être composé uniquement de chiffres et/ou de lettres.",
-  "associated_email": "Adresse Email associée"
+  "associated_email": "Adresse Email associée",
+  "An error occured": "Une erreur s'est produite.",
+  "you_want_to_stop_your_premium_trial_period": "Vous souhaitez arrêter votre période d’essai Opentalent Artist Premium",
+  "stop_trial_period_warning_1a": "En choisissant d’arrêter votre période d'essai, votre compte reviendra automatiquement à la version",
+  "stop_trial_period_warning_1b": "sans perte de vos données essentielles.",
+  "stop_trial_period_warning_2": "Que se passe-t-il si vous arrêtez votre période d’essai ?",
+  "stop_trial_period_warning_3": "Les fonctionnalités premium de l’essai ne seront plus accessibles.",
+  "stop_trial_period_warning_4": "Vos données et configurations Premium sont conservées pendant 30 jours.",
+  "stop_trial_period_warning_5": "Vous pourrez toujours gérer vos activités grâce aux fonctionnalités de la version de base.",
+  "stop_trial_period_warning_6": "Si vous souhaitez continuer à profiter des avantages complets d’Opentalent Artist Premium, vous pouvez souscrire à une licence à tout moment.",
+  "stop_trial_missing_version_label": "précédente",
+  "stop_trial": "Arrêter l'essai",
+  "trial_ongoing": "En cours d'essai",
+  "try_premium_version": "Essayer la version premium",
+  "subscribe_to_the_offer": "Souscrire à l'offre",
+  "to_know_more": "En savoir plus"
 }

+ 19 - 1
layouts/default.vue

@@ -8,7 +8,14 @@
 
       <LayoutHeader />
 
-      <LayoutMainMenu />
+      <LayoutMainMenu>
+        <template #prepend="{ isRail }">
+          <LayoutUpgradePremiumButton
+            v-if="showUpgradePremiumButton"
+            :minimized="isRail"
+          />
+        </template>
+      </LayoutMainMenu>
 
       <v-main class="main">
         <LayoutSubheader />
@@ -29,4 +36,15 @@ import { useLayoutStore } from '~/stores/layout'
 
 const layoutStore = useLayoutStore()
 layoutStore.name = 'default'
+
+const accessProfile = useAccessProfileStore()
+const organizationProfile = useOrganizationProfileStore()
+
+const showUpgradePremiumButton: ComputedRef<boolean> = computed(
+  () =>
+    ((organizationProfile.isArtistProduct ||
+      organizationProfile.isTrialActive) &&
+      (accessProfile.isCaMember || accessProfile.isAdmin)) ??
+    false,
+)
 </script>

+ 3 - 0
models/Organization/OrganizationProfile.ts

@@ -41,4 +41,7 @@ export default class OrganizationProfile extends ApiResource {
 
   @Num(null)
   declare parametersId: null
+
+  @Str(null)
+  declare principalType: string
 }

+ 183 - 183
nuxt.config.ts

@@ -30,13 +30,13 @@ if (process.env.NUXT_ENV === 'dev') {
  * @see https://nuxt.com/docs/api/configuration/nuxt-config
  */
 export default defineNuxtConfig({
- ssr: true,
+  ssr: true,
 
- experimental: {
-   // Fix the 'Cannot stringify non POJO' bug
-   // @see https://github.com/nuxt/nuxt/issues/20787
-   renderJsonPayloads: false,
- },
+  experimental: {
+    // Fix the 'Cannot stringify non POJO' bug
+    // @see https://github.com/nuxt/nuxt/issues/20787
+    renderJsonPayloads: false,
+  },
 
  runtimeConfig: {
    // Private config that is only available on the server
@@ -63,183 +63,183 @@ export default defineNuxtConfig({
    },
  },
 
- hooks: {
-   'builder:watch': console.log,
- },
-
- app: {
-   head: {
-     title: 'Opentalent',
-     meta: [
-       { charset: 'utf-8' },
-       { name: 'viewport', content: 'width=device-width, initial-scale=1' },
-       { name: 'msapplication-TileColor', content: '#324250' },
-       {
-         name: 'msapplication-TileImage',
-         content: '/favicon/favicon-144x144.png',
-       },
-     ],
-     link: [
-       { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '57x57',
-         href: '/favicon/apple-touch-icon-57x57.png',
-       },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '114x114',
-         href: '/favicon/apple-touch-icon-114x114.png',
-       },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '72x72',
-         href: '/favicon/apple-touch-icon-72x72.png',
-       },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '144x144',
-         href: '/favicon/apple-touch-icon-144x144.png',
-       },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '120x120',
-         href: '/favicon/apple-touch-icon-120x120.png',
-       },
-       {
-         rel: 'apple-touch-icon-precomposed',
-         sizes: '152x152',
-         href: '/favicon/apple-touch-icon-152x152.png',
-       },
-       {
-         rel: 'icon',
-         sizes: '32x32',
-         type: 'image/x-icon',
-         href: '/favicon/favicon-32x32.png',
-       },
-       {
-         rel: 'icon',
-         sizes: '16x16',
-         type: 'image/x-icon',
-         href: '/favicon/favicon-16x16.png',
-       },
-     ],
-   },
- },
-
- css: [
-   '@/assets/css/global.scss',
-   '@/assets/css/theme.scss',
-   '@/assets/css/import.scss',
-   '@vuepic/vue-datepicker/dist/main.css',
- ],
-
- typescript: {
-   strict: true,
- },
-
- modules: [
-   // eslint-disable-next-line require-await
-   async (_, nuxt) => {
-     nuxt.hooks.hook('vite:extendConfig', (config) =>
-       // @ts-expect-error A revoir après que les lignes aient été décommentées
-       (config.plugins ?? []).push(
-         vuetify(),
-         // Remplacer par cela quand l'issue https://github.com/vuetifyjs/vuetify-loader/issues/273 sera règlée..
-         // voir aussi : https://github.com/nuxt/nuxt/issues/15412 et https://github.com/vuetifyjs/vuetify-loader/issues/290
-         // voir aussi : https://github.com/jrutila/nuxt3-vuetify3-bug
-         // vuetify({
-         //     styles: { configFile: './assets/css/settings.scss' }
-         // })
-       ),
-     )
-   },
-   [
-     '@pinia/nuxt',
-     {
-       autoImports: [
-         // automatically imports `usePinia()`
-         'defineStore',
-         // automatically imports `usePinia()` as `usePiniaStore()`
-         ['defineStore', 'definePiniaStore'],
-       ],
-     },
-   ],
-   '@pinia-orm/nuxt',
-   '@nuxtjs/i18n',
-   '@nuxt/devtools',
-   '@nuxt/image',
-   'nuxt-prepare',
-   'nuxt-vitalizer',
- ],
-
- vite: {
-   esbuild: {
-     drop: process.env.DEBUG ? [] : ['console', 'debugger'],
-     tsconfigRaw: {
-       compilerOptions: {
-         experimentalDecorators: true,
-       },
-     },
-   },
-   ssr: {
-     // with ssr enabled, this config is required to load vuetify properly
-     noExternal: ['vuetify'],
-   },
-   server: {
-     https,
-     // @ts-expect-error J'ignore pourquoi cette erreur TS se produit, cette propriété est valide
-     port: 443,
-     hmr: {
-       protocol: 'wss',
-       port: 24678,
-     },
-   },
- },
-
- // Hide the sourcemaps warnings with vuetify
- // @see https://github.com/vuetifyjs/vuetify-loader/issues/290#issuecomment-1435702713
- sourcemap: {
-   server: false,
-   client: false,
- },
-
- i18n: {
-   langDir: 'lang',
-   lazy: true,
-   strategy: 'no_prefix',
-   locales: [
-     {
-       code: 'en',
-       iso: 'en-US',
-       file: 'en.json',
-       name: 'English',
-     },
-     {
-       code: 'fr',
-       iso: 'fr-FR',
-       file: 'fr.json',
-       name: 'Français',
-     },
-   ],
-   defaultLocale: 'fr',
-   detectBrowserLanguage: false,
-   vueI18n: './i18n.config.ts',
- },
-
- image: {
-   provider: 'none',
- },
-
- build: {
-   transpile,
- },
-
- ignore: [process.env.NUXT_ENV === 'prod' ? 'pages/dev/*' : ''],
-
- prepare: {
-   scripts: ['prepare/buildIndex.ts'],
- },
+  hooks: {
+    'builder:watch': console.log,
+  },
+
+  app: {
+    head: {
+      title: 'Opentalent',
+      meta: [
+        { charset: 'utf-8' },
+        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+        { name: 'msapplication-TileColor', content: '#324250' },
+        {
+          name: 'msapplication-TileImage',
+          content: '/favicon/favicon-144x144.png',
+        },
+      ],
+      link: [
+        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '57x57',
+          href: '/favicon/apple-touch-icon-57x57.png',
+        },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '114x114',
+          href: '/favicon/apple-touch-icon-114x114.png',
+        },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '72x72',
+          href: '/favicon/apple-touch-icon-72x72.png',
+        },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '144x144',
+          href: '/favicon/apple-touch-icon-144x144.png',
+        },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '120x120',
+          href: '/favicon/apple-touch-icon-120x120.png',
+        },
+        {
+          rel: 'apple-touch-icon-precomposed',
+          sizes: '152x152',
+          href: '/favicon/apple-touch-icon-152x152.png',
+        },
+        {
+          rel: 'icon',
+          sizes: '32x32',
+          type: 'image/x-icon',
+          href: '/favicon/favicon-32x32.png',
+        },
+        {
+          rel: 'icon',
+          sizes: '16x16',
+          type: 'image/x-icon',
+          href: '/favicon/favicon-16x16.png',
+        },
+      ],
+    },
+  },
+
+  css: [
+    '@/assets/css/global.scss',
+    '@/assets/css/theme.scss',
+    '@/assets/css/import.scss',
+    '@vuepic/vue-datepicker/dist/main.css',
+  ],
+
+  typescript: {
+    strict: true,
+  },
+
+  modules: [
+    // eslint-disable-next-line require-await
+    async (_, nuxt) => {
+      nuxt.hooks.hook('vite:extendConfig', (config) =>
+        // @ts-expect-error A revoir après que les lignes aient été décommentées
+        (config.plugins ?? []).push(
+          vuetify(),
+          // Remplacer par cela quand l'issue https://github.com/vuetifyjs/vuetify-loader/issues/273 sera règlée..
+          // voir aussi : https://github.com/nuxt/nuxt/issues/15412 et https://github.com/vuetifyjs/vuetify-loader/issues/290
+          // voir aussi : https://github.com/jrutila/nuxt3-vuetify3-bug
+          // vuetify({
+          //     styles: { configFile: './assets/css/settings.scss' }
+          // })
+        ),
+      )
+    },
+    [
+      '@pinia/nuxt',
+      {
+        autoImports: [
+          // automatically imports `usePinia()`
+          'defineStore',
+          // automatically imports `usePinia()` as `usePiniaStore()`
+          ['defineStore', 'definePiniaStore'],
+        ],
+      },
+    ],
+    '@pinia-orm/nuxt',
+    '@nuxtjs/i18n',
+    '@nuxt/devtools',
+    '@nuxt/image',
+    'nuxt-prepare',
+    'nuxt-vitalizer',
+  ],
+
+  vite: {
+    esbuild: {
+      drop: process.env.DEBUG ? [] : ['console', 'debugger'],
+      tsconfigRaw: {
+        compilerOptions: {
+          experimentalDecorators: true,
+        },
+      },
+    },
+    ssr: {
+      // with ssr enabled, this config is required to load vuetify properly
+      noExternal: ['vuetify'],
+    },
+    server: {
+      https,
+      // @ts-expect-error J'ignore pourquoi cette erreur TS se produit, cette propriété est valide
+      port: 443,
+      hmr: {
+        protocol: 'wss',
+        port: 24678,
+      },
+    },
+  },
+
+  // Hide the sourcemaps warnings with vuetify
+  // @see https://github.com/vuetifyjs/vuetify-loader/issues/290#issuecomment-1435702713
+  sourcemap: {
+    server: false,
+    client: false,
+  },
+
+  i18n: {
+    langDir: 'lang',
+    lazy: true,
+    strategy: 'no_prefix',
+    locales: [
+      {
+        code: 'en',
+        iso: 'en-US',
+        file: 'en.json',
+        name: 'English',
+      },
+      {
+        code: 'fr',
+        iso: 'fr-FR',
+        file: 'fr.json',
+        name: 'Français',
+      },
+    ],
+    defaultLocale: 'fr',
+    detectBrowserLanguage: false,
+    vueI18n: './i18n.config.ts',
+  },
+
+  image: {
+    provider: 'none',
+  },
+
+  build: {
+    transpile,
+  },
+
+  ignore: [process.env.NUXT_ENV === 'prod' ? 'pages/dev/*' : ''],
+
+  prepare: {
+    scripts: ['prepare/buildIndex.ts'],
+  },
 
  compatibilityDate: '2025-03-10'
 })

+ 0 - 1
pages/parameters/subdomains/new.vue

@@ -142,7 +142,6 @@ const rules = () => [
 
 <style scoped lang="scss">
 .validationMessage {
-  font-size: 13px;
   height: 20px;
   min-height: 20px;
 }

File diff suppressed because it is too large
+ 554 - 522
pages/subscription.vue


+ 1 - 1
services/data/normalizer/hydraNormalizer.ts

@@ -171,7 +171,7 @@ class HydraNormalizer {
   }
 
   protected static denormalizeEntity(model: typeof ApiResource, item: AnyJson) {
-    item.id = this.getItemIdValue(model, item)
+    item['id'] = this.getItemIdValue(model, item)
 
     // eslint-disable-next-line new-cap
     const instance = new model(item)

+ 12 - 0
stores/organizationProfile.ts

@@ -23,10 +23,14 @@ export const useOrganizationProfileStore = defineStore(
     const modules: Ref<Array<string>> = ref([])
     const hasChildren: Ref<boolean | null> = ref(false)
     const legalStatus: Ref<string | null> = ref(null)
+    const principalType: Ref<string | null> = ref(null)
     const showAdherentList: Ref<boolean | null> = ref(false)
     const networks: Ref<Array<string>> = ref([])
     const website: Ref<string | null> = ref(null)
     const parents: Ref<Array<BaseOrganizationProfile>> = ref([])
+    const isTrialActive: Ref<boolean> = ref(false)
+    const trialCountDown: Ref<number> = ref(0)
+    const productBeforeTrial: Ref<string | null> = ref(null)
 
     // Getters
     /**
@@ -178,8 +182,12 @@ export const useOrganizationProfileStore = defineStore(
       modules.value = Array.from(profile.modules)
       hasChildren.value = profile.hasChildren
       legalStatus.value = profile.legalStatus
+      principalType.value = profile.principalType
       showAdherentList.value = profile.showAdherentList
+      isTrialActive.value = profile.trialActive
+      trialCountDown.value = profile.trialCountDown
       networks.value = Array.from(profile.networks)
+      productBeforeTrial.value = profile.productBeforeTrial
 
       _.each(profile.parents, (parent) => {
         parents.value.push({
@@ -206,6 +214,7 @@ export const useOrganizationProfileStore = defineStore(
       modules,
       hasChildren,
       legalStatus,
+      principalType,
       showAdherentList,
       networks,
       website,
@@ -223,6 +232,9 @@ export const useOrganizationProfileStore = defineStore(
       isManagerProduct,
       isShowAdherentList,
       isAssociation,
+      isTrialActive,
+      trialCountDown,
+      productBeforeTrial,
       getWebsite,
       hasModule,
       setProfile,

+ 4 - 0
types/interfaces.d.ts

@@ -129,8 +129,12 @@ interface organizationState extends BaseOrganizationProfile {
   isSchool: boolean
   showAdherentList?: boolean | null
   legalStatus?: string | null
+  principalType?: string | null
   networks: Array<string>
   parents: Array<BaseOrganizationProfile>
+  isTrialActive: boolean
+  trialCountDown: number
+  productBeforeTrial?: string | null
 
   hasModule(module: string): boolean
 }

Some files were not shown because too many files changed in this diff