Jelajahi Sumber

Merge branch 'feature/V8-7041-mise--disposition-du-bon-de-comm' into feature/boutique

# Conflicts:
#	pages/subscription.vue
Vincent 8 bulan lalu
induk
melakukan
acd1a3960b

+ 2 - 2
components/Ui/Input/Image.vue

@@ -95,7 +95,7 @@ import { useEntityManager } from '~/composables/data/useEntityManager'
 import { useImageManager } from '~/composables/data/useImageManager'
 import { FILE_VISIBILITY, TYPE_ALERT } from '~/types/enum/enums'
 import { usePageStore } from '~/stores/page'
-import ImageUtils from '~/services/utils/imageUtils'
+import FileUtils from '~/services/utils/fileUtils'
 
 const props = defineProps({
   /**
@@ -312,7 +312,7 @@ const uploadImage = async (event: any) => {
   currentImage.value.id = null
   currentImage.value.name = uploadedFile.name
   currentImage.value.src = URL.createObjectURL(uploadedFile)
-  currentImage.value.content = await ImageUtils.blobToBase64(uploadedFile)
+  currentImage.value.content = await FileUtils.blobToBase64(uploadedFile)
 
   // Met à jour la configuration du cropper
   cropperConfig.value.top = 0

+ 24 - 0
composables/utils/useDownloadFromRoute.ts

@@ -0,0 +1,24 @@
+import FileSaver from 'file-saver'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import File from '~/models/Core/File'
+
+/**
+ * Permet de télécharger un fichier fourni par la route donnée
+ * @param route
+ * @param filename
+ */
+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
+
+  if (!response || response.size === 0) {
+    console.error('Error: no file found at ' + route)
+  }
+
+  const blob = new Blob([response], { type: response.type })
+
+  // eslint-disable-next-line import/no-named-as-default-member
+  FileSaver.saveAs(blob, filename)
+}

+ 2 - 1
models/OnlineRegistration/RegistrationStatus.ts

@@ -1,4 +1,5 @@
-import { Str, Uid } from 'pinia-orm/dist/decorators'
+import { Str, Uid, Num } from 'pinia-orm/dist/decorators'
+import {IdField} from "~/models/decorators";
 import ApiResource from '~/models/ApiResource'
 import {IdField} from "~/models/decorators";
 import {Num} from "pinia-orm/decorators";

+ 213 - 197
nuxt.config.ts

@@ -30,200 +30,216 @@ if (process.env.NUXT_ENV === 'dev') {
  * @see https://nuxt.com/docs/api/configuration/nuxt-config
  */
 export default defineNuxtConfig({
-  ssr: true,
-  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
-    env: '',
-    baseUrl: '',
-    baseUrlLegacy: '',
-    baseUrlAdminLegacy: '',
-    baseUrlTypo3: '',
-    baseUrlMercure: '',
-    fileStorageBaseUrl: '',
-    supportUrl: '',
-    basicomptaUrl: 'https://app.basicompta.fr/',
-    // Config within public will be also exposed to the client
-    public: {
-      env: '',
-      baseUrl: '',
-      baseUrlLegacy: '',
-      baseUrlAdminLegacy: '',
-      baseUrlTypo3: '',
-      baseUrlMercure: '',
-      fileStorageBaseUrl: '',
-      supportUrl: '',
-      basicomptaUrl: 'https://app.basicompta.fr/',
-    },
-  },
-  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'],
-  },
-})
+ ssr: true,
+
+ 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
+   env: '',
+   baseUrl: '',
+   baseUrlLegacy: '',
+   baseUrlAdminLegacy: '',
+   baseUrlTypo3: '',
+   baseUrlMercure: '',
+   fileStorageBaseUrl: '',
+   supportUrl: '',
+   basicomptaUrl: 'https://app.basicompta.fr/',
+   // Config within public will be also exposed to the client
+   public: {
+     env: '',
+     baseUrl: '',
+     baseUrlLegacy: '',
+     baseUrlAdminLegacy: '',
+     baseUrlTypo3: '',
+     baseUrlMercure: '',
+     fileStorageBaseUrl: '',
+     supportUrl: '',
+     basicomptaUrl: 'https://app.basicompta.fr/',
+   },
+ },
+
+ 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-02-07'
+})

+ 42 - 22
pages/subscription.vue

@@ -62,29 +62,35 @@ Page 'Mon abonnement'
             <v-row>
               <v-table v-if="dolibarrAccount !== null">
                 <thead>
-                <tr>
-                  <th>{{ $t('reference') }}</th>
-                  <th>{{ $t('date') }}</th>
-                  <th>{{ $t('taxExcludedAmount') }}</th>
-                  <th>{{ $t('status') }}</th>
-                </tr>
+                  <tr>
+                    <th>{{ $t('reference') }}</th>
+                    <th>{{ $t('date') }}</th>
+                    <th>{{ $t('taxExcludedAmount') }}</th>
+                    <th>{{ $t('status') }}</th>
+                    <th></th>
+                  </tr>
                 </thead>
                 <tbody>
-                <tr v-for="bill in dolibarrAccount.bills" :key="bill.id">
-                  <td>{{ bill.ref }}</td>
-                  <td>{{ $d(bill.date) }}</td>
-                  <td>
-                    {{
-                      bill.taxExcludedAmount.toLocaleString($i18n.locale, {
-                        style: 'currency',
-                        currency: 'EUR',
-                      })
-                    }}
-                  </td>
-                  <td>
-                    {{ bill.paid === true ? $t('paid') : $t('unpaid') }}
-                  </td>
-                </tr>
+                  <tr v-for="bill in dolibarrAccount.bills" :key="bill.id">
+                    <td>{{ bill.ref }}</td>
+                    <td>{{ $d(bill.date) }}</td>
+                    <td>
+                      {{
+                        bill.taxExcludedAmount.toLocaleString($i18n.locale, {
+                          style: 'currency',
+                          currency: 'EUR',
+                        })
+                      }}
+                    </td>
+                    <td>
+                      {{ bill.paid === true ? $t('paid') : $t('unpaid') }}
+                    </td>
+                    <td>
+                      <a @click="downloadDolibarrBill(bill.ref)" class="clickable">
+                        {{ $t('download') }}
+                      </a>
+                    </td>
+                  </tr>
                 </tbody>
               </v-table>
             </v-row>
@@ -300,7 +306,10 @@ import {useOrganizationProfileStore} from '~/stores/organizationProfile'
 import {useEntityFetch} from '~/composables/data/useEntityFetch'
 import DolibarrAccount from '~/models/Organization/DolibarrAccount'
 import MobytUserStatus from '~/models/Organization/MobytUserStatus'
-import UrlUtils from "~/services/utils/urlUtils";
+import UrlUtils from '~/services/utils/urlUtils';
+import {useDownloadFromRoute} from '~/composables/utils/useDownloadFromRoute';
+
+const ability = useAbility()
 import {useApiLegacyRequestService} from "~/composables/data/useApiLegacyRequestService";
 
 //meta
@@ -450,10 +459,21 @@ async function subscription(){
 function stopTrial(){
   showDialogTrialStopConfirmation.value = true
 }
+
+const downloadDolibarrBill = (ref: string): void => {
+  const route = UrlUtils.join('api/dolibarr/download/invoice', ref)
+
+  useDownloadFromRoute(route, `${ref}.pdf`)
+}
 </script>
 
 
 <style scoped lang="scss">
+.clickable {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
 .offer_title{
   span{
     border-radius: 5px;

+ 5 - 0
plugins/error.ts

@@ -0,0 +1,5 @@
+export default defineNuxtPlugin((nuxtApp) => {
+  nuxtApp.vueApp.config.errorHandler = (error, _) => {
+    console.error(error)
+  }
+})

+ 0 - 5
plugins/error.ts.off

@@ -1,5 +0,0 @@
-export default defineNuxtPlugin((nuxtApp) => {
-    nuxtApp.vueApp.config.errorHandler = (error, context) => {
-        console.log(error)
-    }
-})

+ 14 - 18
services/data/entityManager.ts

@@ -178,28 +178,24 @@ class EntityManager {
     id: number,
     forceRefresh: boolean = false,
   ): Promise<ApiResource> {
-    try {
-      // If the model instance is already in the store and forceRefresh is false, return the object in store
-      if (!forceRefresh) {
-        // TODO: est-ce qu'il y a vraiment des situations où on appellera cette méthode sans le forceRefresh?
-        const item = this.find(model, id)
-        if (item && typeof item !== 'undefined') {
-          return item
-        }
+    // If the model instance is already in the store and forceRefresh is false, return the object in store
+    if (!forceRefresh) {
+      // TODO: est-ce qu'il y a vraiment des situations où on appellera cette méthode sans le forceRefresh?
+      const item = this.find(model, id)
+      if (item && typeof item !== 'undefined') {
+        return item
       }
+    }
 
-      // Else, get the object from the API
-      const url = UrlUtils.join('api', model.entity, String(id))
-      const response = await this.apiRequestService.get(url)
+    // Else, get the object from the API
+    const url = UrlUtils.join('api', model.entity, String(id))
+    const response = await this.apiRequestService.get(url)
 
-      // deserialize the response
-      const attributes = HydraNormalizer.denormalize(response, model)
-        .data as object
+    // deserialize the response
+    const attributes = HydraNormalizer.denormalize(response, model)
+      .data as object
 
-      return this.newInstance(model, attributes)
-    } catch (e) {
-      console.error(e)
-    }
+    return this.newInstance(model, attributes)
   }
 
   /**

+ 3 - 3
services/data/imageManager.ts

@@ -1,5 +1,5 @@
 import ApiRequestService from './apiRequestService'
-import ImageUtils from '~/services/utils/imageUtils'
+import FileUtils from '~/services/utils/fileUtils'
 import { FILE_TYPE, FILE_VISIBILITY } from '~/types/enum/enums'
 
 /**
@@ -89,8 +89,8 @@ class ImageManager {
    * @protected
    */
   protected async toBase64(data: BlobPart) {
-    const blob = ImageUtils.newBlob(data)
-    return (await ImageUtils.blobToBase64(blob)) ?? ''
+    const blob = FileUtils.newBlob(data)
+    return (await FileUtils.blobToBase64(blob)) ?? ''
   }
 
   /**

+ 3 - 3
services/utils/imageUtils.ts → services/utils/fileUtils.ts

@@ -1,9 +1,9 @@
 /**
  * Manipulation des images
  */
-class ImageUtils {
+class FileUtils {
   /**
-   * Returns a blob with the given data and the image filetype
+   * Returns a blob with the given data and the file's type
    *
    * @param data
    * @param filetype
@@ -24,4 +24,4 @@ class ImageUtils {
     })
   }
 }
-export default ImageUtils
+export default FileUtils

+ 4 - 4
tests/units/services/utils/imageUtils.test.ts → tests/units/services/utils/fileUtils.test.ts

@@ -1,17 +1,17 @@
 import { describe, test, it, expect } from 'vitest'
-import ImageUtils from '~/services/utils/imageUtils'
+import FileUtils from '~/services/utils/fileUtils'
 import 'blob-polyfill'
 
 describe('newBlob', () => {
   test('defaultFiletype', async () => {
-    const blob = ImageUtils.newBlob('test')
+    const blob = FileUtils.newBlob('test')
 
     expect(await blob.text()).toEqual('test')
     expect(await blob.type).toEqual('image/jpeg')
   })
 
   test('otherFiletype', async () => {
-    const blob = ImageUtils.newBlob('test', 'image/png')
+    const blob = FileUtils.newBlob('test', 'image/png')
 
     expect(await blob.text()).toEqual('test')
     expect(await blob.type).toEqual('image/png')
@@ -22,7 +22,7 @@ describe('blobToBase64', () => {
   test('simple blog', async () => {
     const blob = new Blob(['foo' as BlobPart], { type: 'image/jpeg' })
 
-    expect(await ImageUtils.blobToBase64(blob)).toEqual(
+    expect(await FileUtils.blobToBase64(blob)).toEqual(
       'data:image/jpeg;base64,Zm9v',
     )
   })