Преглед изворни кода

Merge branch 'add_events_settings_menu_in_create_dialog' into release/2.3.beta

Olivier Massot пре 2 година
родитељ
комит
7d82448268

+ 138 - 0
components/Layout/Header/UniversalCreation/Card.vue

@@ -0,0 +1,138 @@
+<!--
+  VCard proposant une option dans la boite de dialogue "Assistant de création"
+
+  La carte peut prendre en paramètres des options `to` et `href` :
+  Si `to` est défini et pas `href`, la carte mène simplement à un nouveau menu dans le wizard.
+  Si `href` est défini, et pas `to`, la carte se comporte comme un lien, et doit rediriger la page vers `href` au clic.
+  Enfin, si les deux sont définis, l'url est envoyée au composant parent pour être mémorisée, et la carte mène ensuite à
+  un nouveau menu dans le wizard, qui permettra d'affiner l'url, par exemple en lui ajoutant une query.
+-->
+
+<template>
+  <v-card
+    class="col-md-6"
+    color=""
+    flat
+    border="solid 1px"
+    @click="onClick"
+  >
+    <v-row no-gutters style="height: 100px">
+      <v-col cols="3" class="flex-grow-0 flex-shrink-0 d-flex justify-center">
+        <v-icon
+            :icon="icon"
+            size="50"
+            class="ma-2 pa-2 align-self-center text-neutral-strong"
+        />
+      </v-col>
+      <v-col
+          cols="9"
+          align-self="center"
+          class="pl-2 infos-container flex-grow-1 flex-shrink-1"
+      >
+        <h4 class="text-primary">{{ $t(title) }}</h4>
+        <p class="text-neutral-strong">
+          {{ $t(textContent) }}
+        </p>
+      </v-col>
+    </v-row>
+  </v-card>
+</template>
+
+<script setup lang="ts">
+  import {PropType} from "@vue/runtime-core";
+  import {MENU_LINK_TYPE} from "~/types/enum/layout";
+  import {useAdminUrl} from "~/composables/utils/useAdminUrl";
+  import UrlUtils from "~/services/utils/urlUtils";
+
+  const props = defineProps({
+    /**
+     * Target location in the wizard
+     */
+    to: {
+      type: String,
+      required: false,
+      default: null
+    },
+    /**
+     * Target url
+     */
+    href: {
+      type: String,
+      required: false,
+      default: null
+    },
+    /**
+     * Target url
+     */
+    linkType: {
+      type: Number as PropType<MENU_LINK_TYPE>,
+      required: false,
+      default: MENU_LINK_TYPE.V1
+    },
+    /**
+     * Title displayed on the card
+     */
+    title: {
+      type: String,
+      required: true
+    },
+    /**
+     * Description displayed on the card
+     */
+    textContent: {
+      type: String,
+      required: true
+    },
+    /**
+     * Icon displayed on the card
+     */
+    icon: {
+      type: String,
+      required: true
+    }
+  })
+
+  const emit = defineEmits(['click'])
+
+  const { makeAdminUrl } = useAdminUrl()
+
+  let url: string | null = null;
+
+  if (props.href !== null) {
+    switch (props.linkType) {
+      case MENU_LINK_TYPE.V1:
+        url = makeAdminUrl(props.href)
+        break;
+      case MENU_LINK_TYPE.EXTERNAL:
+        url = UrlUtils.prependHttps(props.href)
+        break;
+      default:
+        url = props.href
+    }
+  }
+
+  const onClick = () => {
+    emit('click', props.to, url)
+  }
+</script>
+
+<style lang="scss" scoped>
+  h4 {
+    font-size: 15px;
+    font-weight: bold;
+    margin-bottom: 6px;
+  }
+
+  p {
+    font-size: 13px;
+  }
+
+  .infos-container {
+    padding: 15px 0;
+  }
+
+  .v-card:hover {
+    cursor: pointer;
+    background: rgb(var(--v-theme-primary-alt));
+  }
+</style>

+ 113 - 28
components/Layout/Header/UniversalCreation/CreateButton.vue

@@ -1,5 +1,5 @@
 <!--
-  Bouton Créer du header de l'application
+  Bouton Créer du header de l'application et boite de dialogue associée
 -->
 
 <template>
@@ -25,20 +25,22 @@
       <span>{{ $t('create') }}</span>
     </v-btn>
 
-    <LayoutDialog :show="showing" :max-width="850" >
+    <LayoutDialog :show="showCreateDialog" :max-width="850" >
       <template #dialogType>{{ $t('creative_assistant') }}</template>
 
       <template #dialogTitle>
-        <span v-if="type === 'home'">{{ $t('what_do_you_want_to_create') }}</span>
-        <span v-else-if="type === 'access'">{{ $t('what_type_of_contact_do_you_want_to_create') }}</span>
-        <span v-else-if="type === 'event'">{{ $t('what_do_you_want_to_add_to_your_planning') }}</span>
-        <span v-else-if="type === 'message'">{{ $t('what_do_you_want_to_send') }}</span>
+        <span v-if="location === 'home'">{{ $t('what_do_you_want_to_create') }}</span>
+        <span v-else-if="location === 'access'">{{ $t('what_type_of_contact_do_you_want_to_create') }}</span>
+        <span v-else-if="location === 'event'">{{ $t('what_do_you_want_to_add_to_your_planning') }}</span>
+        <span v-else-if="location === 'message'">{{ $t('what_do_you_want_to_send') }}</span>
+        <span v-else-if="location === 'event-params'">{{ $t('which_date_and_which_hour') }}</span>
       </template>
 
       <template #dialogText>
          <LayoutHeaderUniversalCreationGenerateCardsSteps
-             :step="step"
-             @updateStep="updateStep"
+             :path="path"
+             @cardClick="onCardClick"
+             @urlUpdate="onUrlUpdate"
          />
       </template>
 
@@ -48,9 +50,13 @@
             {{ $t('cancel') }}
           </v-btn>
 
-          <v-btn v-if="step > 1" class="theme-neutral-soft" @click="reset" >
+          <v-btn v-if="path.length > 1" class="theme-neutral-soft" @click="goToPrevious" >
             {{ $t('previous_step') }}
           </v-btn>
+
+          <v-btn v-if="targetUrl !== null && !directRedirectionOngoing" class="theme-primary" @click="validate" >
+            {{ $t('validate') }}
+          </v-btn>
         </div>
       </template>
     </LayoutDialog>
@@ -60,40 +66,119 @@
 <script setup lang="ts">
   import {Ref, ref} from "@vue/reactivity";
   import {useDisplay} from "vuetify";
+  import {ComputedRef} from "vue";
+  import {usePageStore} from "~/stores/page";
 
-  const showing: Ref<Boolean> = ref(false);
-  const step: Ref<Number> = ref(1);
-  const type: Ref<String> = ref('home');
+  const { mdAndDown: asIcon } = useDisplay()
 
-  const updateStep = ({stepChoice, typeChoice}: any) =>{
-    step.value = stepChoice
-    type.value = typeChoice
-  }
+  // Set to true to show the Create dialog
+  const showCreateDialog: Ref<boolean> = ref(false);
+
+  // The succession of menus the user has been through; used to keep track of the navigation
+  const path: Ref<Array<string>> = ref(['home'])
 
+  // The current menu
+  const location: ComputedRef<string> = computed(() => {
+    return path.value.at(-1) ?? 'home'
+  })
+
+  // The current target URL (@see onUrlUpdate())
+  const targetUrl: Ref<string | null> = ref(null)
+
+  // Already redirecting (to avoid the display of the 'validate' button when page has already been redirected and is loading)
+  const directRedirectionOngoing: Ref<boolean> = ref(false)
+
+  /**
+   * Return to the home menu
+   */
   const reset = () => {
-    step.value = 1
-    type.value = 'home'
+    path.value = ['home']
+  }
+
+  /**
+   * Go back to the previous step
+   */
+  const goToPrevious = () => {
+    if (path.value.length === 1) {
+      return
+    }
+    path.value.pop()
   }
 
+  /**
+   * Display the create dialog
+   */
   const show = () => {
     reset()
-    showing.value = true
+    showCreateDialog.value = true
   }
 
+  const pageStore = usePageStore()
+
+  /**
+   * Redirect the user to the given url
+   * @param url
+   */
+  const redirect = (url: string) => {
+    pageStore.loading = true
+    window.location.href = url
+  }
+
+  /**
+   * Go to the current targetUrl
+   */
+  const validate = () => {
+    if (targetUrl.value === null) {
+      console.warn('No url defined')
+      return
+    }
+    redirect(targetUrl.value)
+  }
+
+  /**
+   * Close the Create dialog
+   */
   const close = () => {
-    showing.value = false
+    showCreateDialog.value = false
   }
 
-  const { mdAndDown: asIcon } = useDisplay()
+  /**
+   * A cart has been clicked. The reaction depends on the card's properties.
+   *
+   * @param to  Target location in the wizard
+   * @param href  Target absolute url
+   */
+  const onCardClick = (to: string | null, href: string | null) => {
+    if (to !== null) {
+      // La carte définit une nouvelle destination : on se dirige vers elle.
+      path.value.push(to)
+
+    } else if (href !== null) {
+      // La carte définit une url avec href, et pas de nouvelle destination : on suit directement le lien pour éviter
+      // l'étape de validation devenue inutile.
+      directRedirectionOngoing.value = true
+      redirect(href)
+
+    } else {
+      console.warn('Error: card has no `to` nor `href` defined')
+    }
+  }
 
+  /**
+   * The url has been updated in the GenerateCardsStep component
+   * @param url
+   */
+  const onUrlUpdate = (url: string) => {
+    targetUrl.value = url
+  }
 </script>
 
 <style scoped lang="scss">
-:deep(.v-btn .v-icon) {
-  font-size: 16px !important;
-}
-:deep(.v-btn) {
-  text-transform: none !important;
-  font-weight: 600;
-}
+  :deep(.v-btn .v-icon) {
+    font-size: 16px !important;
+  }
+  :deep(.v-btn) {
+    text-transform: none !important;
+    font-weight: 600;
+  }
 </style>

+ 119 - 0
components/Layout/Header/UniversalCreation/EventParams.vue

@@ -0,0 +1,119 @@
+<!--
+Event parameters page in the create dialog
+-->
+
+<template>
+  <v-container class="pa-4">
+    <v-row class="align-center">
+      <v-col cols="2">
+        <span>{{ $t('start_on') }}</span>
+      </v-col>
+
+      <v-col cols="6">
+        <UiDatePicker v-model="eventStart" with-time />
+      </v-col>
+    </v-row>
+
+    <v-row v-show="eventStart < now" class="anteriorDateWarning mt-0">
+      <v-col cols="2" class="pt-1"></v-col>
+      <v-col cols="9" class="pt-1">
+        <i class="fa fa-circle-info" /> {{ $t('please_note_that_this_reservation_start_on_date_anterior_to_now') }}
+      </v-col>
+    </v-row>
+
+    <v-row class="align-center">
+      <v-col cols="2">
+        <span>{{ $t('for_time') }}</span>
+      </v-col>
+
+      <v-col cols="10" class="d-flex flex-row align-center">
+        <UiInputNumber v-model="eventDurationDays" class="mx-3" :min="0" />
+        <span>{{ $t('day(s)') }}</span>
+
+        <UiInputNumber v-model="eventDurationHours" class="mx-3" :min="0" />
+        <span>{{ $t('hour(s)') }}</span>
+
+        <UiInputNumber v-model="eventDurationMinutes" class="mx-3" :min="0" />
+        <span>{{ $t('minute(s)') }}</span>
+      </v-col>
+
+    </v-row>
+
+    <v-row>
+      <v-col cols="2">
+        <span>{{ $t('ending_date') }}</span>
+      </v-col>
+
+      <v-col cols="6" class="endDate">
+        <span>{{ formattedEventEnd }}</span>
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script setup lang="ts">
+  import {ref, Ref} from "@vue/reactivity";
+  import {add, format, startOfHour, formatISO} from "date-fns";
+  import {ComputedRef} from "vue";
+  import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
+
+  const i18n = useI18n()
+
+  // An event is sent each time the resulting params are updated
+  const emit = defineEmits(['paramsUpdated'])
+
+  // Get the start of the next hour as a default event start
+  const now: Date = new Date()
+  const eventStart: Ref<Date> = ref(startOfHour(add(now, { 'hours': 1 })))
+
+  const eventDurationDays: Ref<number> = ref(0)
+  const eventDurationHours: Ref<number> = ref(1)
+  const eventDurationMinutes: Ref<number> = ref(0)
+
+  // Duration of the events, in minutes
+  const eventDuration: ComputedRef<number> = computed(() => {
+    return (eventDurationDays.value * 24 * 60) + (eventDurationHours.value * 60) + eventDurationMinutes.value
+  })
+
+  // Event end
+  const eventEnd: ComputedRef<Date> = computed(() => add(eventStart.value, { 'minutes': eventDuration.value }))
+
+  const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
+  const formattedEventEnd: ComputedRef<string> = computed(() => {
+    return format(eventEnd.value, 'EEEE dd MMMM yyyy HH:mm', {locale: fnsLocale})
+  })
+
+  // Build the event params
+  const params: ComputedRef<{'start': string, 'end': string}> = computed(() => {
+    return {
+      'start': formatISO(eventStart.value),
+      'end': formatISO(eventEnd.value),
+    }
+  })
+
+  // Send an update event as soon as the page is mounted
+  onMounted(() => {
+    emit('paramsUpdated', params.value)
+  })
+
+  // Send an update event every time the params change
+  const unwatch = watch(params, (newParams) => {
+    emit('paramsUpdated', newParams)
+  })
+  onUnmounted(() => {
+    unwatch()
+  })
+</script>
+
+<style scoped lang="scss">
+  .endDate {
+    font-weight: 600;
+    text-transform: capitalize;
+    color: rgb(var(--v-theme-on-neutral));
+  }
+
+  .anteriorDateWarning {
+    color: rgb(var(--v-theme-info));
+    font-weight: 600;
+  }
+</style>

+ 165 - 83
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -3,220 +3,280 @@
 -->
 
 <template>
-  <v-container v-if="step === 1">
+
+  <!-- Menu Accueil -->
+  <v-container v-if="location === 'home'">
     <v-row>
+
+      <!-- Une personne -->
       <v-col cols="6" v-if="ability.can('manage', 'users')">
-          <LayoutHeaderUniversalCreationTypeCard
-            title="a_person"
-            text-content="add_new_person_student"
-            icon="fa fa-user"
-            type="access"
-            @typeClick="onTypeClick"
+          <LayoutHeaderUniversalCreationCard
+              to="access"
+              title="a_person"
+              text-content="add_new_person_student"
+              icon="fa fa-user"
+              @click="onCardClick"
           />
       </v-col>
 
+      <!-- Un évènement -->
       <v-col cols="6" v-if="ability.can('display', 'agenda_page')
                 && (
                    ability.can('display', 'course_page') ||
                    ability.can('display', 'exam_page') ||
                    ability.can('display', 'pedagogics_project_page')
                 )">
-        <LayoutHeaderUniversalCreationTypeCard
-          title="an_event"
-          text-content="add_an_event_course"
-          icon="fa fa-calendar-alt"
-          type="event"
-          @typeClick="onTypeClick"
+        <LayoutHeaderUniversalCreationCard
+            to="event"
+            title="an_event"
+            text-content="add_an_event_course"
+            icon="fa fa-calendar-alt"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Autre évènement -->
       <v-col cols="6" v-else-if="ability.can('display', 'agenda_page') && ability.can('manage', 'events')">
-        <LayoutHeaderUniversalCreationTypeCard
-          title="other_event"
-          text-content="other_event_text_creation_card"
-          icon="far fa-calendar"
-          :link="makeAdminUrl('/calendar/create/events')"
+        <LayoutHeaderUniversalCreationCard
+            to="event-params"
+            title="other_event"
+            text-content="other_event_text_creation_card"
+            icon="far fa-calendar"
+            href="/calendar/create/events"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Une correspondance -->
       <v-col cols="6" v-if="ability.can('display', 'message_send_page')
                    && (
                     ability.can('manage', 'emails') ||
                     ability.can('manage', 'mails') ||
                     ability.can('manage', 'texto')
                   )">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
+          to="message"
           title="a_correspondence"
-          text-content="sen_email_letter"
+          text-content="send_email_letter"
           icon="fa fa-comment"
           type="message"
-          @typeClick="onTypeClick"
+          @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un matériel (direct link) -->
       <v-col cols="6" v-if="ability.can('manage', 'equipments')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
           title="a_materiel"
           text-content="add_any_type_material"
           icon="fa fa-cube"
-          :link="makeAdminUrl('/list/create/equipment')"
+          href="/list/create/equipment"
+          @click="onCardClick"
         />
       </v-col>
     </v-row>
   </v-container>
 
-  <v-container v-if="step === 2">
-    <v-row v-if="type === 'access'">
+  <!-- Menu "Créer une personne" -->
+  <v-container v-if="location === 'access'">
+    <v-row>
+      <!-- Un adhérent -->
       <v-col cols="6" v-if="isLaw1901">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="an_adherent"
             text-content="adherent_text_creation_card"
             icon="fa fa-user"
-            :link="makeAdminUrl('/universal_creation_person/adherent')"
+            href="/universal_creation_person/adherent"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un membre du CA -->
       <v-col cols="6" v-if="isLaw1901">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_ca_member"
             text-content="ca_member_text_creation_card"
             icon="fa fa-users"
-            :link="makeAdminUrl('/universal_creation_person/ca_member')"
+            href="/universal_creation_person/ca_member"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un élève -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_student"
             text-content="student_text_creation_card"
             icon="fa fa-user"
-            :link="makeAdminUrl('/universal_creation_person/student')"
+            href="/universal_creation_person/student"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un tuteur -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_guardian"
             text-content="guardian_text_creation_card"
             icon="fa fa-female"
-            :link="makeAdminUrl('/universal_creation_person/guardian')"
+            href="/universal_creation_person/guardian"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un professeur -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_teacher"
             text-content="teacher_text_creation_card"
             icon="fa fa-graduation-cap"
-            :link="makeAdminUrl('/universal_creation_person/teacher')"
+            href="/universal_creation_person/teacher"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un membre du personnel -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_member_of_staff"
             text-content="personnel_text_creation_card"
             icon="fa fa-suitcase"
-            :link="makeAdminUrl('/universal_creation_person/personnel')"
+            href="/universal_creation_person/personnel"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Une entité légale -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_legal_entity"
             text-content="moral_text_creation_card"
             icon="fa fa-building"
-            :link="makeAdminUrl('/universal_creation_person/company')"
+            href="/universal_creation_person/company"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Une inscription en ligne -->
       <v-col cols="6" v-if="hasOnlineRegistrationModule">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="online_registration"
             text-content="online_registration_text_creation_card"
             icon="fa fa-list-alt"
-            :link="makeAdminUrl('/online/registration/new_registration')"
+            href="/online/registration/new_registration"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un autre type de contact -->
       <v-col cols="6">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="another_type_of_contact"
             text-content="other_contact_text_creation_card"
             icon="fa fa-plus"
-            :link="makeAdminUrl('/universal_creation_person/other_contact')"
+            href="/universal_creation_person/other_contact"
+            @click="onCardClick"
         />
       </v-col>
     </v-row>
+  </v-container>
 
-    <v-row v-if="type === 'event'">
-      <!-- /?start=2023-06-12T08:00:00%2B0200&end=2023-06-12T09:00:00%2B0200 -->
+  <!-- Menu Évènement -->
+  <v-container v-if="location === 'event'">
+    <v-row>
+      <!-- Un cours -->
       <v-col cols="6" v-if="ability.can('display', 'course_page')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
+            to="event-params"
+            href="/calendar/create/courses"
             title="course"
             text-content="course_text_creation_card"
             icon="fa fa-users"
-            :link="makeAdminUrl('/calendar/create/courses', {'start': eventDefaultStart, 'end': eventDefaultEnd})"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un examen -->
       <v-col cols="6" v-if="ability.can('display', 'exam_page')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
+            to="event-params"
+            href="/calendar/create/examens"
             title="exam"
             text-content="exam_text_creation_card"
             icon="fa fa-graduation-cap"
-            :link="makeAdminUrl('/calendar/create/examens')"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un projet pédagogique -->
       <v-col cols="6" v-if="ability.can('display', 'pedagogics_project_page')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
+            to="event-params"
+            href="/calendar/create/educational_projects"
             title="educational_services"
             text-content="educational_services_text_creation_card"
             icon="fa fa-suitcase"
-            :link="makeAdminUrl('/calendar/create/educational_projects')"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un autre évènement -->
       <v-col cols="6" v-if="ability.can('manage', 'events')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
+            to="event-params"
+            href="/calendar/create/events"
             title="other_event"
             text-content="other_event_text_creation_card"
             icon="far fa-calendar"
-            :link="adminLegacy + '/calendar/create/events'"
+            @click="onCardClick"
         />
       </v-col>
     </v-row>
+  </v-container>
 
-    <v-row v-if="type === 'message'">
+  <!-- Une correspondance -->
+  <v-container v-if="location === 'message'">
+    <v-row>
+      <!-- Un email -->
       <v-col cols="6" v-if="ability.can('manage', 'emails')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="an_email"
             text-content="email_text_creation_card"
             icon="far fa-envelope"
-            :link="makeAdminUrl('/list/create/emails')"
+            href="/list/create/emails"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un courrier -->
       <v-col cols="6" v-if="ability.can('manage', 'mails')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_letter"
             text-content="letter_text_creation_card"
             icon="far fa-file-alt"
-            :link="makeAdminUrl('/list/create/mails')"
+            href="/list/create/mails"
+            @click="onCardClick"
         />
       </v-col>
 
+      <!-- Un SMS -->
       <v-col cols="6" v-if="ability.can('manage', 'texto')">
-        <LayoutHeaderUniversalCreationTypeCard
+        <LayoutHeaderUniversalCreationCard
             title="a_sms"
             text-content="sms_text_creation_card"
             icon="fa fa-mobile-alt"
-            :link="makeAdminUrl('/list/create/sms')"
+            href="/list/create/sms"
+            @click="onCardClick"
         />
       </v-col>
     </v-row>
   </v-container>
+
+  <!-- Page de pré-paramétrage des évènements -->
+  <LayoutHeaderUniversalCreationEventParams
+      v-if="location === 'event-params'"
+      @params-updated="onEventParamsUpdated"
+  />
 </template>
 
 <script setup lang="ts">
@@ -224,43 +284,65 @@
   import {useOrganizationProfileStore} from "~/stores/organizationProfile";
   import {useAbility} from "@casl/vue";
   import {ComputedRef} from "vue";
+  import {useAdminUrl} from "~/composables/utils/useAdminUrl";
   import UrlUtils from "~/services/utils/urlUtils";
-  import {add, formatISO, startOfHour} from "date-fns";
 
   const props = defineProps({
-    step: {
-      type: Number,
+    /**
+     * The path that the user followed troughout the wizard
+     */
+    path: {
+      type: Array<string>,
       required: true
     }
   })
 
-  const emit = defineEmits(['updateStep'])
+  const location: ComputedRef<string> = computed(() => {
+    return props.path.at(-1) ?? 'home'
+  })
 
   const ability = useAbility()
 
-  const type: Ref<String> = ref('');
   const organizationProfile = useOrganizationProfileStore()
-  const runtimeConfig = useRuntimeConfig()
+  const isLaw1901: ComputedRef<boolean> = organizationProfile.isAssociation
+  const hasOnlineRegistrationModule: Ref<boolean> = ref(organizationProfile.hasModule('IEL'))
+
+  const baseUrl: Ref<string | null> = ref(null)
+  const query: Ref<Record<string, string>> = ref({})
+
+  const url: ComputedRef<string | null> = computed(() => {
+    if (baseUrl.value === null) {
+      return null
+    }
+    return UrlUtils.addQuery(baseUrl.value, query.value)
+  })
 
-  // Get the start of the next hour as a default event start
-  const now: Date = new Date()
-  const eventDefaultStart: string = formatISO(startOfHour(add(now, { 'hours': 1 })))
-  const eventDefaultEnd: string = formatISO(startOfHour(add(now, { 'hours': 2 })))
+  const emit = defineEmits(['cardClick', 'urlUpdate'])
 
-  const onTypeClick = (step: Number, Cardtype: String) => {
-    type.value = Cardtype;
-    emit('updateStep', { stepChoice: step, typeChoice: Cardtype });
+  /**
+   * Called when a card is clicked
+   * @param to  Target location in the wizard
+   * @param href  Target absolute url
+   */
+  const onCardClick = (to: string | null, href: string | null) => {
+    if (href !== null) {
+      baseUrl.value = href
+    }
+    emit('cardClick', to, url.value)
   }
 
-  const adminLegacy: Ref<string> = ref(runtimeConfig.baseUrlAdminLegacy)
-  const isLaw1901: ComputedRef<boolean> = organizationProfile.isAssociation
-  const hasOnlineRegistrationModule: ComputedRef<boolean> = computed(
-      () => organizationProfile.hasModule('IEL')
-  )
-
-  const makeAdminUrl = (tail: string, query: Record<string, string> = {}) => {
-    let url = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#', tail)
-    url = UrlUtils.addQuery(url, query)
-    return url
+  /**
+   * Called when the event parameters page is updated
+   * @param event
+   */
+  const onEventParamsUpdated = (event: {'start': string, 'end': string}) => {
+    query.value = event
   }
+
+  const unwatch = watch(url, (newUrl: string | null) => {
+    emit('urlUpdate', newUrl)
+  })
+  onUnmounted(() => {
+    unwatch()
+  })
 </script>

+ 0 - 80
components/Layout/Header/UniversalCreation/TypeCard.vue

@@ -1,80 +0,0 @@
-<!--
-  VCard proposant une option dans la boite de dialogue "Assistant de création"
--->
-
-<template>
-  <v-card
-    class="col-md-6"
-    color=""
-    flat
-    border="solid 1px"
-    :href="link"
-    @click="$emit('typeClick', 2, type)"
-  >
-    <v-row no-gutters style="height: 100px">
-      <v-col cols="3" class="flex-grow-0 flex-shrink-0 d-flex justify-center">
-        <v-icon
-            :icon="icon"
-            size="50"
-            class="ma-2 pa-2 align-self-center text-neutral-strong"
-        />
-      </v-col>
-      <v-col
-          cols="9"
-          align-self="center"
-          class="pl-2 infos-container flex-grow-1 flex-shrink-1"
-      >
-        <h4 class="text-primary">{{ $t(title) }}</h4>
-        <p class="text-neutral-strong">
-          {{ $t(textContent) }}
-        </p>
-      </v-col>
-    </v-row>
-  </v-card>
-</template>
-
-<script setup lang="ts">
-  const props = defineProps({
-    title: {
-      type: String,
-      required: true
-    },
-    textContent: {
-      type: String,
-      required: true
-    },
-    icon: {
-      type: String,
-      required: true
-    },
-    link: {
-      type: String,
-      required: false
-    },
-    type: {
-      type: String,
-      required: false
-    }
-  })
-</script>
-
-<style lang="scss" scoped>
-  h4 {
-    font-size: 15px;
-    font-weight: bold;
-    margin-bottom: 6px;
-  }
-
-  p {
-    font-size: 13px;
-  }
-
-  .infos-container {
-    padding: 15px 0;
-  }
-
-  .v-card:hover {
-    cursor: pointer;
-    background: rgb(var(--v-theme-primary-alt));
-  }
-</style>

+ 1 - 1
components/Layout/SubHeader/DataTimingRange.vue

@@ -5,7 +5,7 @@
         {{ $t('period_choose') }}
       </span>
 
-      <UiInputDateRangePicker
+      <UiDateRangePicker
           :model-value="datesRange"
           :max-height="28"
           @update:model-value="updateDateTimeRange"

+ 71 - 0
components/Ui/DatePicker.vue

@@ -0,0 +1,71 @@
+<!--
+Sélecteur de dates
+
+@see https://vuetifyjs.com/en/components/date-pickers/
+-->
+
+<template>
+  <main>
+    <!-- @see https://vue3datepicker.com/props/modes/#multi-calendars -->
+    <VueDatePicker
+        :model-value="modelValue"
+        :locale="i18n.locale.value"
+        :format-locale="fnsLocale"
+        :format="dateFormat"
+        :enable-time-picker="withTime"
+        :teleport="true"
+        text-input
+        :auto-apply="true"
+        :select-text="$t('select')"
+        :cancel-text="$t('cancel')"
+        @update:model-value="onUpdate"
+    />
+  </main>
+</template>
+
+<script setup lang="ts">
+import {computed} from "@vue/reactivity";
+import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
+import {PropType} from "@vue/runtime-core";
+
+const i18n = useI18n()
+
+const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
+const defaultFormatPattern = DateUtils.getFormatPattern(i18n.locale.value as supportedLocales)
+
+const props = defineProps({
+  modelValue: {
+    type: Object as PropType<Date>,
+    required: false,
+    default: null
+  },
+  readonly: {
+    type: Boolean,
+    required: false,
+    default: false
+  },
+  format: {
+    type: String,
+    required: false,
+    default: null
+  },
+  withTime: {
+    type: Boolean,
+    required: false,
+    default: false
+  }
+})
+
+const dateFormat: Ref<string> = ref(props.format ?? defaultFormatPattern)
+
+const emit = defineEmits(['update:model-value'])
+
+const onUpdate = (event: Date) => {
+  emit('update:model-value', event)
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 0 - 0
components/Ui/Input/DateRangePicker.vue → components/Ui/DateRangePicker.vue


+ 107 - 0
components/Ui/Input/Number.vue

@@ -0,0 +1,107 @@
+<!--
+An input for numeric values
+-->
+
+<template>
+  <v-text-field
+      ref="input"
+      :modelValue.number="modelValue"
+      hide-details
+      single-line
+      :density="density"
+      type="number"
+      @update:modelValue="modelValue = keepInRange(cast($event)); emitUpdate()"
+  />
+</template>
+
+<script setup lang="ts">
+
+import {PropType} from "@vue/runtime-core";
+
+type Density = null | 'default' | 'comfortable' | 'compact';
+
+const props = defineProps({
+  modelValue: {
+    type: Number,
+    required: true
+  },
+  default: {
+    type: Number,
+    required: false,
+    default: 0
+  },
+  min: {
+    type: Number,
+    required: false,
+    default: null
+  },
+  max: {
+    type: Number,
+    required: false,
+    default: null
+  },
+  density: {
+    type: String as PropType<Density>,
+    required: false,
+    default: 'default'
+  }
+})
+
+/**
+ * Reference to the v-text-field
+ */
+const input: Ref<any> = ref(null)
+
+/**
+ * Cast the value to a number, or fallback on default value
+ * @param val
+ */
+const cast = (val: number | string): number => {
+  val = Number(val)
+  if (isNaN(val)) {
+    return props.default
+  }
+
+  return val
+}
+
+/**
+ * Ensure the value is between min and max values
+ * @param val
+ */
+const keepInRange = (val: number) => {
+  if (props.min !== null && props.max !== null && props.min >= props.max) {
+    console.warn('Number input: minimum value is greater than maximum value')
+  }
+  if (props.min !== null && val < props.min) {
+    val = props.min
+  }
+  if (props.max !== null && val > props.max) {
+    val = props.max
+  }
+  return val
+}
+
+
+const emit = defineEmits(['update:modelValue'])
+
+/**
+ * Emit the update event
+ */
+const emitUpdate = () => {
+  emit('update:modelValue', props.modelValue)
+}
+
+/**
+ * Setup min and max values at the input level
+ */
+onMounted(() => {
+  const inputElement = input.value.$el.querySelector('input')
+  if (props.min !== null) {
+    inputElement.min = props.min
+  }
+  if (props.max !== null) {
+    inputElement.max = props.max
+  }
+})
+</script>

+ 0 - 0
composables/utils/useAbilityUtils.ts


+ 14 - 0
composables/utils/useAdminUrl.ts

@@ -0,0 +1,14 @@
+import UrlUtils from "~/services/utils/urlUtils";
+
+export const useAdminUrl = () => {
+    const runtimeConfig = useRuntimeConfig()
+
+    const makeAdminUrl = (tail: string, query: Record<string, string> = {}): string => {
+        const baseUrl = runtimeConfig.baseUrlAdminLegacy ?? runtimeConfig.public.baseUrlAdminLegacy
+        let url = UrlUtils.join(baseUrl, '#', tail)
+        url = UrlUtils.addQuery(url, query)
+        return url
+    }
+
+    return { makeAdminUrl }
+}

+ 11 - 2
lang/fr.json

@@ -347,7 +347,7 @@
   "previous_step": "Étape précédente",
   "add_any_type_material": "Ajoutez tout type de matériel ou de documents tels que des partitions à votre parc de matériel",
   "a_materiel": "Un matériel",
-  "sen_email_letter": "Envoyez un email, un courrier, ou un SMS aux personnes de votre carnet d'adresses",
+  "send_email_letter": "Envoyez un email, un courrier, ou un SMS aux personnes de votre carnet d'adresses",
   "a_correspondence": "Une correspondance",
   "add_an_event_course": "Ajoutez un évènement, un cours, une prestation pédagogique, un examen... à votre planning",
   "an_event": "Un évènement",
@@ -577,5 +577,14 @@
   "Internal Server Error": "Erreur de serveur interne",
   "cmf_licence_breadcrumbs": "Licence CMF",
   "online_registration": "Inscription en ligne",
-  "online_registration_text_creation_card": "Ajouter une nouvelle inscription"
+  "online_registration_text_creation_card": "Ajouter une nouvelle inscription",
+  "start_on": "Débute le",
+  "for_time": "Durant",
+  "day(s)": "jour(s)",
+  "hour(s)": "heure(s)",
+  "minute(s)": "minute(s)",
+  "ending_date": "Date de fin",
+  "please_note_that_this_reservation_start_on_date_anterior_to_now": "Veuillez noter que cette réservation débute à une date antérieure à aujourd'hui",
+  "validate": "Valider",
+  "which_date_and_which_hour": "A quelle date et quelle heure ?"
 }

+ 8 - 0
services/utils/dateUtils.ts

@@ -54,6 +54,14 @@ export default class DateUtils {
     return mapping[code] ?? mapping[defaultLocale]
   }
 
+  public static getFormatPattern(code: supportedLocales): string {
+    const mapping = {
+      'en': 'MM/dd/yyyy HH:mm',
+      'fr': 'dd/MM/yyyy HH:mm'
+    }
+    return mapping[code] ?? mapping[defaultLocale]
+  }
+
   public static formatIsoShortDate(date: Date): string {
     return format(date, 'yyyy-MM-dd')
   }