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

Merge branch 'feature/V8-4029-restaurer-la-selection-des-dates' into develop

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

+ 21 - 57
components/Layout/SubHeader/DataTimingRange.vue

@@ -1,18 +1,14 @@
 <template>
   <main class="d-flex align-center data-timing-range">
     <div v-if="show" class="d-flex align-center" style="max-height: 100%">
-      <span class="pl-2 mr-2 font-weight-bold on-neutral">
+      <span class="pl-2 mr-2 font-weight-bold on-neutral" style="min-width: 150px;">
         {{ $t('period_choose') }}
       </span>
 
-      <UiInputDatePicker
-        label="date_choose"
-        :data="datesRange"
-        range
-        dense
-        single-line
-        height="22"
-        @update="updateDateTimeRange"
+      <UiInputDateRangePicker
+          :model-value="datesRange"
+          :max-height="28"
+          @update:model-value="updateDateTimeRange"
       />
     </div>
 
@@ -40,6 +36,7 @@ import {useFormStore} from "~/stores/form";
 import {WatchStopHandle} from "@vue/runtime-core";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import Access from "~/models/Access/Access";
+import DateUtils from "~/services/utils/dateUtils";
 
 const btn: Ref = ref(null)
 const show: Ref<boolean> = ref(false)
@@ -48,17 +45,26 @@ const { setDirty } = useFormStore()
 const accessProfileStore = useAccessProfileStore()
 const { em } = useEntityManager()
 
-const datesRange: Ref<Array<string | null | undefined>> = ref([
-  accessProfileStore.historical.dateStart,
-  accessProfileStore.historical.dateEnd
-])
+const start = accessProfileStore.historical.dateStart
+const end = accessProfileStore.historical.dateEnd
 
-const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
+const datesRange: Ref<Array<Date> | null> = ref((start && end) ? [new Date(start), new Date(end)] : null)
+
+const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
   if (accessProfileStore.id === null) {
     throw new Error('Invalid profile id')
   }
 
-  accessProfileStore.setHistoricalRange(dates[0], dates[1])
+  datesRange.value = dates
+
+  if (datesRange.value !== null && datesRange.value[0] !== null && datesRange.value[1] !== null) {
+    accessProfileStore.setHistoricalRange(
+        DateUtils.formatIsoShortDate(datesRange.value[0]),
+        DateUtils.formatIsoShortDate(datesRange.value[1])
+    )
+  } else {
+    accessProfileStore.setHistorical(false, true, false)
+  }
   setDirty(false)
 
   await em.patch(
@@ -95,46 +101,4 @@ if (accessProfileStore.historical.dateStart || accessProfileStore.historical.dat
   border-width: 1px 1px 1px 0;
   border-style: solid;
 }
-
-.data-timing-range {
-  max-height: 22px;
-
-  :deep(.v-text-field) {
-    padding-top: 0 !important;
-    margin-top: 0 !important;
-  }
-
-  :deep(.v-input) {
-    max-height: 22px;
-    min-height: 22px;
-    height: 22px;
-  }
-
-  :deep(.v-icon) {
-    max-height: 22px;
-  }
-
-  :deep(.v-field__input) {
-    font-size: 14px;
-    width: 400px !important;
-    padding: 0;
-    max-height: 22px;
-    min-height: 22px;
-    height: 22px;
-  }
-
-  :deep(.v-field-label) {
-    top: 0;
-    font-size: 14px;
-    max-height: 22px;
-    min-height: 22px;
-    height: 22px;
-  }
-
-  :deep(.v-input__prepend) {
-    padding-top: 0;
-    font-size: 14px;
-  }
-}
-
 </style>

+ 16 - 7
components/Layout/Subheader.vue

@@ -16,16 +16,24 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
       <span class="flex-fill" />
 
       <v-card
-        class="d-flex flex-row pt-2 mr-6 align-baseline"
+        class="d-flex flex-row align-center mr-6"
         flat
         tile
       >
         <LayoutSubHeaderActivityYear v-if="smAndUp && !showDateTimeRange" class="activity-year" />
 
         <div v-if="hasMenuOrIsTeacher" class="d-flex flex-row">
-          <LayoutSubHeaderDataTiming v-if="smAndUp && !showDateTimeRange" class="data-timing ml-2" />
-          <LayoutSubHeaderDataTimingRange v-if="smAndUp" class="data-timing-range ml-n1" @showDateTimeRange="showDateTimeRange=$event" />
-          <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2" />
+          <LayoutSubHeaderDataTiming
+              v-if="smAndUp && !showDateTimeRange"
+              class="data-timing ml-2"
+          />
+
+          <LayoutSubHeaderDataTimingRange
+              v-if="smAndUp"
+              class="data-timing-range ml-n1"
+              @showDateTimeRange="showDateTimeRange=$event"
+          />
+          <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2 d-flex align-center" />
         </div>
       </v-card>
     </v-card>
@@ -33,7 +41,6 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 </template>
 
 <script setup lang="ts">
-
     import {useAccessProfileStore} from "~/stores/accessProfile";
     import {computed, ComputedRef, ref, Ref} from "@vue/reactivity";
     import {useMenu} from "~/composables/layout/useMenu";
@@ -47,8 +54,10 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
         () => hasMenu('Main') || (accessProfile.isTeacher ?? false)
     )
 
-    const showDateTimeRange: Ref<boolean> = ref(false)
-
+    const showDateTimeRange: Ref<boolean> = ref(
+        accessProfile.historical.dateStart !== null &&
+        accessProfile.historical.dateEnd !== null
+    )
 </script>
 
 <style scoped lang="scss">

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

@@ -0,0 +1,98 @@
+<template>
+  <!-- @see https://vue3datepicker.com/props/modes/#multi-calendars -->
+  <VueDatePicker
+      :model-value="modelValue"
+      range
+      multi-calendars
+      :auto-apply="autoApply"
+      :locale="i18n.locale.value"
+      :format-locale="fnsLocale"
+      :format="dateFormatPattern"
+      :start-date="today"
+      :teleport="true"
+      :alt-position="dateRangePickerAltPosition"
+      :enable-time-picker="false"
+      close-on-scroll
+      text-input
+      :select-text="$t('select')"
+      :cancel-text="$t('cancel')"
+      input-class-name="date-range-picker-input"
+      @update:model-value="updateDateTimeRange"
+      class="date-range-picker"
+      :style="style"
+  />
+</template>
+
+<script setup lang="ts">
+import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
+
+const props = defineProps({
+  modelValue: {
+    type: Array,
+    required: false,
+    default: null
+  },
+  maxHeight: {
+    type: Number,
+    required: false,
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const autoApply = false
+
+const updateDateTimeRange = (value: [string, string]) => {
+  emit('update:modelValue', value)
+}
+
+const i18n = useI18n()
+
+const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
+const dateFormatPattern = DateUtils.getShortFormatPattern(i18n.locale.value as supportedLocales)
+
+const today = new Date()
+
+let style = '';
+if (props.maxHeight !== null) {
+  style += 'height: ' + props.maxHeight + 'px;max-height: ' + props.maxHeight + 'px;'
+}
+
+/**
+ * Recalcule la position du panneau de sélection des dates si trop près du bord droit de l'écran
+ * @param el
+ */
+const dateRangePickerAltPosition = (el: HTMLElement) => {
+  let xOffset = 0
+  const fullWidth = 500
+  const rightPadding = 30
+  const rect = el.getBoundingClientRect()
+
+  if ((rect.left + fullWidth + rightPadding) > window.innerWidth) {
+    xOffset = window.innerWidth - (rect.left + fullWidth + rightPadding)
+  }
+
+  return {
+    top: rect.bottom,
+    left: rect.left + xOffset
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  :deep(div[role="textbox"]) {
+    height: 100% !important;
+    max-height: 100% !important;
+  }
+
+  :deep(.dp__input_wrap) {
+    height: 100% !important;
+    max-height: 100% !important;
+  }
+
+  :deep(.date-range-picker-input) {
+      height: 100% !important;
+      max-height: 100% !important;
+  }
+</style>

+ 3 - 1
lang/fr.json

@@ -570,5 +570,7 @@
   "PENDING": "En cours de traitement",
   "READY": "Prêt",
   "DELETED": "Supprimé",
-  "ERROR": "Erreur"
+  "ERROR": "Erreur",
+  "select": "Sélectionner",
+  "Internal Server Error": "Erreur de serveur interne"
 }

+ 5 - 4
nuxt.config.ts

@@ -1,6 +1,6 @@
 import fs from 'fs';
 import vuetify from 'vite-plugin-vuetify'
-import {NuxtI18nOptions} from "@nuxtjs/i18n";
+import {NuxtI18nOptions} from "@nuxtjs/i18n"
 
 /**
  * Nuxt configuration
@@ -67,7 +67,8 @@ export default defineNuxtConfig({
     css: [
         '@/assets/css/global.scss',
         '@/assets/css/theme.scss',
-        '@/assets/css/import.scss'
+        '@/assets/css/import.scss',
+        '@vuepic/vue-datepicker/dist/main.css'
     ],
     typescript: {
         strict: true
@@ -150,7 +151,7 @@ export default defineNuxtConfig({
         vueI18n: {
             legacy: false,
             datetimeFormats: {
-                'fr-FR': {
+                'fr': {
                     short: {
                         year: 'numeric', month: 'numeric', day: 'numeric'
                     },
@@ -172,6 +173,6 @@ export default defineNuxtConfig({
         },
     } as NuxtI18nOptions,
     build: {
-        transpile: ['vuetify'],
+        transpile: ['vuetify', '@vuepic/vue-datepicker'],
     },
 })

+ 23 - 22
package.json

@@ -22,64 +22,65 @@
   },
   "dependencies": {
     "@casl/ability": "^6.3.3",
-    "@casl/vue": "2.1.1",
+    "@casl/vue": "2.2.1",
     "@fortawesome/fontawesome-free": "^6.3.0",
     "@mdi/font": "^7.0.96",
     "@nuxt/image": "^0.7.1",
-    "@nuxt/image-edge": "^1.0.0-27919678.2f5b64b",
-    "@nuxtjs/i18n": "^8.0.0-beta.9",
+    "@nuxt/image-edge": "^1.0.0-27968280.9739e4d",
+    "@nuxtjs/i18n": "^8.0.0-beta.10",
     "@pinia-orm/nuxt": "^1.1.7",
-    "@pinia/nuxt": "0.4.6",
+    "@pinia/nuxt": "0.4.7",
     "@types/file-saver": "^2.0.5",
     "@types/js-yaml": "^4.0.5",
     "@types/vue-the-mask": "^0.11.1",
+    "@vuepic/vue-datepicker": "^4.2.1",
     "cleave.js": "^1.6.0",
     "date-fns": "^2.29.3",
     "event-source-polyfill": "^1.0.31",
     "file-saver": "^2.0.5",
     "js-yaml": "^4.1.0",
-    "libphonenumber-js": "1.10.19",
-    "nuxt": "^3.2.0",
+    "libphonenumber-js": "1.10.24",
+    "nuxt": "^3.3.1",
     "nuxt-lodash": "^2.4.1",
-    "pinia": "^2.0.30",
+    "pinia": "^2.0.33",
     "pinia-orm": "^1.5.1",
-    "sass": "^1.57.1",
+    "sass": "^1.59.3",
     "uuid": "^9.0.0",
     "vite-plugin-vuetify": "^1.0.1",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
-    "vuetify": "3.1.6",
+    "vuetify": "3.1.9",
     "yaml-import": "^2.0.0"
   },
   "devDependencies": {
-    "@nuxt/devtools": "^0.1.2",
-    "@nuxt/test-utils": "^3.2.0",
-    "@nuxt/test-utils-edge": "^3.0.1-rc.0-27909581.f711046",
+    "@nuxt/devtools": "^0.2.5",
+    "@nuxt/test-utils": "^3.3.1",
+    "@nuxt/test-utils-edge": "^3.3.2-27981332.886cca19",
     "@nuxtjs/eslint-config": "^12.0.0",
     "@nuxtjs/eslint-config-typescript": "^12.0.0",
-    "@nuxtjs/eslint-module": "^3.1.0",
+    "@nuxtjs/eslint-module": "^4.0.2",
     "@types/cleave.js": "^1.4.7",
     "@types/event-source-polyfill": "^1.0.1",
-    "@types/jest": "^29.4.0",
+    "@types/jest": "^29.4.2",
     "@types/lodash": "^4.14.189",
     "@types/uuid": "^9.0.0",
-    "@typescript-eslint/eslint-plugin": "^5.51.0",
-    "@typescript-eslint/parser": "^5.51.0",
+    "@typescript-eslint/eslint-plugin": "^5.55.0",
+    "@typescript-eslint/parser": "^5.55.0",
     "@vitejs/plugin-vue": "^4.0.0",
-    "@vitest/coverage-c8": "^0.28.4",
+    "@vitest/coverage-c8": "^0.29.2",
     "@vue/eslint-config-standard": "^8.0.1",
-    "@vue/test-utils": "^2.2.10",
+    "@vue/test-utils": "^2.3.1",
     "blob-polyfill": "^7.0.20220408",
-    "eslint": "^8.34.0",
-    "eslint-config-prettier": "^8.5.0",
+    "eslint": "^8.36.0",
+    "eslint-config-prettier": "^8.7.0",
     "eslint-plugin-nuxt": "^4.0.0",
     "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-vue": "^9.7.0",
-    "jsdom": "^21.0.0",
+    "jsdom": "^21.1.1",
     "prettier": "^2.8.4",
     "ts-jest": "^29.0.3",
     "typescript": "4.9.5",
-    "vitest": "0.28.5",
+    "vitest": "0.29.2",
     "vue-jest": "^3.0.7"
   }
 }

+ 3 - 0
pages/poc/blank.vue

@@ -1,4 +1,7 @@
 <template>
+  <div>
+    ...
+  </div>
 </template>
 
 <script setup lang="ts">

+ 6 - 0
plugins/vue-date-picker.ts

@@ -0,0 +1,6 @@
+import VueDatePicker from '@vuepic/vue-datepicker';
+import '@vuepic/vue-datepicker/dist/main.css'
+
+export default defineNuxtPlugin((nuxtApp) => {
+    nuxtApp.vueApp.component('VueDatePicker', VueDatePicker)
+})

+ 29 - 0
services/utils/dateUtils.ts

@@ -1,5 +1,14 @@
 import { format } from 'date-fns';
 import ArrayUtils from "~/services/utils/arrayUtils";
+import { enUS, fr } from 'date-fns/locale'
+
+export const enum supportedLocales {
+  FR = 'fr',
+  EN = 'en'
+}
+
+const defaultLocale = 'fr'
+
 
 export default class DateUtils {
 
@@ -28,4 +37,24 @@ export default class DateUtils {
   public static sort(dates: Array<Date>, reverse: boolean = false): Array<Date> {
     return ArrayUtils.sort(dates, reverse) as Array<Date>
   }
+
+  public static getFnsLocale(code: supportedLocales): Locale {
+    const mapping = {
+      'en': enUS,
+      'fr' : fr
+    }
+    return mapping[code] ?? mapping[defaultLocale]
+  }
+
+  public static getShortFormatPattern(code: supportedLocales): string {
+    const mapping = {
+      'en': 'MM/dd/yyyy',
+      'fr': 'dd/MM/yyyy'
+    }
+    return mapping[code] ?? mapping[defaultLocale]
+  }
+
+  public static formatIsoShortDate(date: Date): string {
+    return format(date, 'yyyy-MM-dd')
+  }
 }

+ 57 - 2
tests/units/services/utils/dateUtils.test.ts

@@ -1,5 +1,5 @@
 import { describe, test, it, expect } from 'vitest'
-import DateUtils from "~/services/utils/dateUtils";
+import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
 
 describe('format', () => {
     test('simple formatting', () => {
@@ -49,6 +49,61 @@ describe('formatDatesAndConcat', () => {
 
 describe('sortDate', () => {
     test('sort', () => {
-        // TODO: should assert that ArrayUtils.sort is called
+        test('simple array', () => {
+            const date1 = new Date(2023, 0, 10)
+            const date2 = new Date(2023, 0, 14)
+            const date3 = new Date(2023, 0, 12)
+
+            const input = [date1, date2, date3]
+
+            const result = DateUtils.sort(input)
+
+            expect(result).toEqual([date1, date3, date2])
+        })
+
+        test('simple array reverse', () => {
+            const date1 = new Date(2023, 0, 10)
+            const date2 = new Date(2023, 0, 14)
+            const date3 = new Date(2023, 0, 12)
+
+            const input = [date1, date2, date3]
+
+            const result = DateUtils.sort(input)
+
+            expect(result).toEqual([date2, date3, date1])
+        })
+    })
+})
+
+describe('getFnsLocale', () => {
+    test('standard call', () => {
+        expect(DateUtils.getFnsLocale(supportedLocales.FR).code).toEqual('fr')
+        expect(DateUtils.getFnsLocale(supportedLocales.EN).code).toEqual('en-US')
+    })
+
+    test('unsupported locale', () => {
+        // @ts-ignore
+        expect(DateUtils.getFnsLocale('xx').code).toEqual('fr')
+    })
+})
+
+describe('getShortFormatPattern', () => {
+    test('standard call', () => {
+        expect(DateUtils.getShortFormatPattern(supportedLocales.FR)).toEqual('dd/MM/yyyy')
+        expect(DateUtils.getShortFormatPattern(supportedLocales.EN)).toEqual('MM/dd/yyyy')
+    })
+
+    test('unsupported locale', () => {
+        // @ts-ignore
+        expect(DateUtils.getShortFormatPattern('xx')).toEqual('dd/MM/yyyy')
     })
 })
+
+describe('formatIsoShortDate', () => {
+    test('standard call', () => {
+        expect(DateUtils.formatIsoShortDate(new Date(2023, 0, 10))).toEqual('2023-01-10')
+        expect(DateUtils.formatIsoShortDate(new Date(2023, 11, 20))).toEqual('2023-12-20')
+    })
+})
+
+

Разлика између датотеке није приказан због своје велике величине
+ 621 - 99
yarn.lock


Неке датотеке нису приказане због велике количине промена