瀏覽代碼

update form POC, add alerts (ongoing), include translations

Olivier Massot 3 年之前
父節點
當前提交
2926f1101f

+ 41 - 0
components/Layout/Alert/Container.vue

@@ -0,0 +1,41 @@
+<!--
+Container principal pour l'affichage d'une ou plusieurs alertes
+-->
+
+<template>
+  <main class="alertContainer">
+    <LayoutAlertContent
+      v-for="(alert, key) in alerts"
+      :key="key"
+      class="alertContent"
+      :alert="alert"
+    />
+  </main>
+</template>
+
+<script setup lang="ts">
+import { Alert } from '~/types/interfaces'
+import {usePageStore} from "~/store/page";
+import {ComputedRef} from "@vue/reactivity";
+
+const pageStore = usePageStore()
+
+const alerts: ComputedRef<Array<Alert>> = computed(() => {
+  return pageStore.$state.alerts
+})
+
+</script>
+
+<style scoped>
+  .alertContainer {
+    position: fixed;
+    bottom: 0;
+    right: 20px;
+    z-index: 1000;
+  }
+
+  .alertContainer > .alertContent {
+    position: relative;
+    margin-bottom: 10px;
+  }
+</style>

+ 73 - 0
components/Layout/Alert/Content.vue

@@ -0,0 +1,73 @@
+<!-- Message d'alerte -->
+
+<template>
+  <v-alert
+    v-model="show"
+    :type="alert.type"
+    class="position"
+    border="left"
+    width="400"
+    dismissible
+    transition="fade-transition"
+    @mouseover="onMouseOver"
+    @mouseout="onMouseOut"
+  >
+    <ul v-if="alert.messages.length > 1">
+       <li v-for="message in alert.messages">
+        {{ $t(message) }}
+      </li>
+    </ul>
+    <span v-else>{{ $t(alert.messages[0]) }}</span>
+  </v-alert>
+</template>
+
+<script setup lang="ts">
+import {Alert} from '~/types/interfaces'
+import {Ref} from "@vue/reactivity";
+import {usePageStore} from "~/store/page";
+
+const $t = (s: string) => { return s } // TODO: remove after i18n install
+
+const props = defineProps({
+    alert: {
+      type: Object as () => Alert,
+      required: true
+    }
+})
+
+const show: Ref<boolean> = ref(true)
+let timeout: any = null
+
+const pageStore = usePageStore()
+
+/**
+ * Retire l'alerte après `time` (en ms)
+ * @param time
+ */
+const clearAlert = (time: number = 2000) => {
+  timeout = setTimeout(() => {
+    show.value = false
+    pageStore.removeSlowlyAlert()
+  }, time)
+}
+
+/**
+ * Réinitialise et suspend le délai avant le retrait de l'alerte au survol du curseur
+ */
+const onMouseOver = () => {
+  clearTimeout(timeout)
+}
+
+/**
+ * Relance le timer avant le retrait de l'alerte lorsque le curseur quitte l'alerte
+ */
+const onMouseOut = () => {
+  clearAlert(2000)
+}
+
+clearAlert()
+
+</script>
+
+<style scoped>
+</style>

+ 7 - 10
composables/data/useAp2iRequestService.ts

@@ -2,10 +2,9 @@ import {useProfileAccessStore} from "~/store/profile/access";
 import {FetchContext, FetchOptions} from "ohmyfetch";
 import PageStore from "~/services/store/pageStoreHelper";
 import {TYPE_ALERT} from "~/types/enums";
-import {useAppConfig, useRuntimeConfig} from "#app";
-import OhMyFetchConnector from "~/services/data/connector/ohMyFetchConnector";
+import {useRuntimeConfig} from "#app";
 import ApiRequestService from "~/services/data/apiRequestService";
-import {AssociativeArray} from "~/services/data/data";
+import {AssociativeArray} from "~/types/data";
 
 /**
  * Retourne une instance de ApiRequestService configurée pour interroger l'api Ap2i
@@ -57,12 +56,12 @@ export const useAp2iRequestService = () => {
             // navigateTo('/login')
             console.error('Unauthorized')
         }
-        if (response && response.status === 403) {
+        else if (response && response.status === 403) {
             new PageStore().addAlerts(TYPE_ALERT.ALERT, ['forbidden'])
-            console.error('forbidden')
+            console.error('Forbidden')
         }
-
-        if (response && response.status === 500) {
+        else if (response && response.status >= 404) {
+            // @see https://developer.mozilla.org/fr/docs/Web/HTTP/Status
             new PageStore().addAlerts(TYPE_ALERT.ALERT, [error ? error.message : response.statusText])
             console.error(error ? error.message : response.statusText)
         }
@@ -77,7 +76,5 @@ export const useAp2iRequestService = () => {
     // Utilise la fonction `create` de ohmyfetch pour générer un fetcher dédié à l'interrogation de Ap2i
     const fetcher = $fetch.create(config)
 
-    const connector = new OhMyFetchConnector(fetcher)
-
-    return new ApiRequestService(connector)
+    return new ApiRequestService(fetcher)
 }

+ 9 - 0
lang/breadcrumbs/fr-FR.js

@@ -0,0 +1,9 @@
+export default (context, locale) => {
+  return ({
+    item: 'Détails',
+    organization_breadcrumbs: 'Fiche de la structure',
+    subscription_breadcrumbs: 'Mon abonnement',
+    address_breadcrumbs: 'Adresse postale',
+    contact_points_breadcrumbs: 'Points de contact'
+  })
+}

+ 19 - 0
lang/content/parameters/fr-FR.js

@@ -0,0 +1,19 @@
+export default (context, locale) => {
+  return ({
+    'help_super_admin': 'Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise surtout pour la gestion de votre site internet et, à la première connexion au logiciel, afin de créer des comptes pour tous membres de votre structure. Enfin, il peut également être utile en cas de dépannage dans certaines situations particulières.',
+    yourWebsiteAddressIs: 'L\'adresse de votre site web est',
+    areYourSureYouWantToDisableYourOpentalentWebsite: 'Êtes-vous sûr(e) de vouloir désactiver votre site web Opentalent',
+    youRegisteredTheFollowingSubdomain: 'Vous avez enregistré le sous-domaine suivant',
+    subdomainIsCurrentlyActive: 'Le sous-domaine est actuellement actif',
+    doYouWantToActivateThisSubdomain: 'Voulez-vous activer ce sous-domaine',
+    activate: 'Activer',
+    active: 'Actif',
+    pleaseEnterYourNewSubdomain: 'Veuillez saisir votre nouveau sous-domaine',
+    subdomain_need_to_have_0_to_60_cars: 'Le sous-domaine doit comporter de 2 à 60 caractères',
+    this_subdomain_is_already_in_use: 'Ce sous-domaine est déjà utilisé',
+    this_subdomain_is_available: 'Ce sous-domaine est disponible',
+    subdomain_can_not_contain_spaces_or_special_cars: 'Le sous-domaine ne doit pas contenir d\'espaces ni de caractères spéciaux',
+    please_enter_a_value_for_the_subdomain: 'Veuillez saisir une valeur pour le sous-domaine',
+    validation_ongoing: 'Validation en cours',
+  })
+}

+ 44 - 0
lang/content/subscription/fr-FR.js

@@ -0,0 +1,44 @@
+/**
+ * Specific translations for the /subscription page
+ *
+ * @param context
+ * @param locale
+ * @returns {{get_more_functionalities_with_version: string, only_for_cmf_members: string, contact_us_at: string, contact_us_for_show_and_demo: string, starting_from_x_eur_ttc_per_month: string, download_order_form: string, for_x_sms: string, for_only_x_eur_ttc_by_month: string, example: string, domain_name: string, and_benefit: string, public_price_x_ttc_a_year: string, product_sheet: string, get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month: string, dummy_domain_name: string, website: string, version_x_up_to_x_students: string, download_cmf_order_form: string, send_sms_from_app_to_your_members: string, freely_try_our_software: string, starting_from_x_eur_ttc_per_ssm: string, yearly_paid_giving_x_eur_ttc_per_year: string, excluding_license_and_training_fees: string, a_suitable_solution_for_your_artistic_school: string, dummy_email_address: string, associated_mail_address: string, switch_to_version: string, or_by_mail_at: string, of_accounts_for_teachers_and_students: string, of_a_complete_website: string}}
+ */
+export default (context, locale) => {
+  return ({
+    until: 'Jusqu\'au',
+    get_more_functionalities_with_version: 'Bénéficiez de plus de fonctionnalités avec la version',
+    for_only_x_eur_ttc_by_month: 'Pour seulement {price} TTC par mois',
+    convert_price_to_sms: 'soit {nb_sms} SMS',
+    yearly_paid_giving_x_eur_ttc_per_year: 'Payable annuellement, soit {price} TTC / an',
+    only_for_cmf_members: 'Offre réservée aux adhérents CMF',
+    public_price_x_ttc_a_year: 'Prix public: {price} TTC/an',
+    product_sheet: 'Fiche produit',
+    download_order_form: 'Télécharger le bon de commande',
+    download_cmf_order_form: 'Télécharger le bon de commande CMF',
+    a_suitable_solution_for_your_artistic_school: 'Une solution économique adaptée à votre établissement d\'enseignement artistique',
+    starting_from_x_eur_ttc_per_month: 'A partir de {price} TTC par mois',
+    version_x_up_to_x_students: 'Version {product} jusqu\'à {max_students} étudiants',
+    excluding_license_and_training_fees: 'Hors frais de licence d\'utilisation et de formation',
+    freely_try_our_software: 'Essayez notre logiciel en toute liberté',
+    contact_us_for_show_and_demo: 'Contactez-nous sans plus tarder pour obtenir une présentation ainsi qu\'un accès de démonstration',
+    contact_us_at: 'Contactez-nous au',
+    or_by_mail_at: 'ou par mail à l\'adresse',
+    switch_to_version: 'Passez à la version',
+    and_benefit: 'et bénéficiez',
+    of_accounts_for_teachers_and_students: 'de comptes pour vos professeurs et élèves',
+    of_a_complete_website: 'd\'un site internet complet',
+    send_sms: 'Envoyez des SMS',
+    to_your_members_from_app: 'à vos membres / élèves depuis votre logiciel',
+    starting_from_x_eur_ttc_per_sms: 'A partir de {price} TTC / sms',
+    for_x_sms: 'pour {amount} SMS',
+    get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month: 'Bénéficiez de votre propre nom de domaine et 5 adresses email pour seulement {price} TTC / mois',
+    example: 'Exemple',
+    domain_name: 'Nom de domaine',
+    dummy_domain_name: 'ma-structure.fr',
+    associated_mail_address: 'Adresse email associée',
+    dummy_email_address: 'contact@ma-structure.fr',
+    sms: 'SMS'
+  })
+}

+ 120 - 0
lang/enum/fr-FR.js

@@ -0,0 +1,120 @@
+export default (context, locale) => {
+  return ({
+    GUARDIANS: 'Tuteurs uniquement',
+    STUDENTS: 'Élèves uniquement',
+    STUDENTS_AND_THEIR_GUARDIANS: 'Élèves et leurs tuteurs',
+    ANNUAL: 'Annuel',
+    HALF: 'Semestriel',
+    QUARTERLY: 'Trimestriel',
+    MONTHLY: 'Mensuel',
+    BY_EDUCATION: 'Par enseignement',
+    BY_TEACHER: 'Par professeur',
+    CATEGORY_ORCHESTRE: 'Orchestre',
+    CATEGORY_AMBULATORY: 'Musique ambulatoire',
+    CATEGORY_OTHER: 'Autres activités',
+    CATEGORY_CHORUS: 'Chorale / Groupe vocal',
+    CATEGORY_BAND: 'Ensemble',
+    BRASS_BAND: 'Brass band',
+    HUNTING_HORNS: 'Trompes de chasse',
+    PHILHARMONIC_ORCHESTRA: 'Orchestre philharmonique',
+    ACCORDION_ORCHESTRA: 'Orchestre d\'accordéons',
+    HARMONY_ORCHESTRA: 'Orchestre d\'harmonie',
+    ORCHESTRA_CLASS: 'Classe d\'orchestre',
+    SYMPHONY_ORCHESTRA: 'Orchestre symphonique',
+    STRING_ORCHESTRA: 'Orchestre à cordes',
+    PLUCKED_ORCHESTRA: 'Orchestre à plectres',
+    FANFARE_BAND: 'Orchestre de fanfare',
+    BAGAD: 'Bagad',
+    BANDAS: 'Bandas ou Fanfare de rue',
+    BATTERY_FANFARE: 'Batterie fanfare',
+    BATTUCADA: 'Battucada',
+    FOLKLORIC_BAND: 'Ensemble folklorique',
+    FIFE_AND_DRUM: 'Fifres et tambours',
+    MARCHING_BAND: 'Marching band ou Show parade',
+    CHILDRENS_CHOIR: 'Choeur d\'enfants',
+    FEMAL_CHOIR: 'Choeur de femmes',
+    MENS_CHOIR: 'Choeur d\'hommes',
+    MIXED_CHORUS: 'Choeur mixte',
+    VOCAL_BAND_UP_16: 'Ensemble vocal (jusqu\'à 16)',
+    CLARINET_CHOIR: 'Ensemble de clarinettes',
+    COPPER_BAND: 'Ensemble de cuivres',
+    FLUTE_ENSEMBLE: 'Ensemble de flûtes',
+    SAXOPHONES_BAND: 'Ensemble de saxophones',
+    VIOLIN_BAND: 'Ensemble de violons',
+    PERCUSSION_BAND: 'Ensemble de percussions',
+    CURRENT_MUSIC_GROUP: 'Groupe de Musique actuelle',
+    CHAMBER_MUSIC_ENSEMBLE: 'Ensemble de Musique de chambre',
+    TRADITIONAL_MUSIC_ENSEMBLE: 'Ensemble de Musique traditionnelle',
+    JAZZ_BAND: 'Ensemble de Jazz',
+    EDUCATION: 'Enseignement',
+    CHEERLEADER: 'Majorettes',
+    TROOP: 'Troupe',
+    BIG_BAND: 'Big band',
+    PRODUCT_ARTIST: 'Opentalent Artist',
+    PRODUCT_ARTIST_PREMIUM: 'Opentalent Artist Premium',
+    PRODUCT_SCHOOL: 'Opentalent School',
+    PRODUCT_SCHOOL_PREMIUM: 'Opentalent School Premium',
+    PRODUCT_MANAGER: 'Opentalent Manager',
+    LOCAL_AUTHORITY: 'Collectivité territoriale (Mairie, SIVOM, SIVU, EPIC, …)',
+    ASSOCIATION_LAW_1901: 'Association loi 1901 ou assimilée (Droit local, ...)',
+    COMMERCIAL_SOCIETY: 'Entreprise commerciale (SARL, SAS, EURL, Autoentrepreneur, …)',
+    ARTISTIC_EDUCATION_ONLY: 'Enseignement artistique seul',
+    ARTISTIC_PRACTICE_EDUCATION: 'Pratique et enseignement artistique',
+    ARTISTIC_PRACTICE_ONLY: 'Pratique artistique seule',
+    DELEGATION: 'Délégation',
+    DEPARTEMENTAL_FEDERATION: 'Fédération départementale',
+    GROUPMENT: 'Groupement',
+    LOCAL_FEDERATION: 'Fédération locale',
+    MUSIC_OPENTALENT: 'Opentalent',
+    NATIONAL_FEDERATION: 'Fédération nationale',
+    REGIONAL_FEDERATION: 'Fédération régionale',
+    CESMD: 'CESMD Centre d\'études supérieures de musique et de danse',
+    CNSMD: 'CNSMD Conservatoire national supérieur de musique',
+    CRC: 'CRC Conservatoire à rayonnement communal',
+    CRD: 'CRD Conservatoire à rayonnement départemental',
+    CRI: 'CRI Conservatoire à rayonnement intercommunal',
+    CRR: 'CRR Conservatoire à rayonnement régional',
+    EENC: 'EENC Établissement d\'enseignement artistique non classé',
+    EMP: 'EMP Ecole de musique privée',
+    MULTIPLE: 'Multiple',
+    UNIQUE: 'Unique',
+    MAIN_BUILDING: 'Etablissement principal',
+    SECONDARY_SCHOOL: 'Etablissement secondaire',
+    ACTALIANS: 'Actalians',
+    AFDAS: 'Afdas',
+    AGEFOS: 'Agefos',
+    AGEFOS_PME: 'Agefos pme',
+    ANFA: 'Anfa',
+    ANFH: 'Anfh',
+    APCMA: 'Apcma',
+    CNFPT: 'Cnfpt',
+    CONSTRUCTYS: 'Constructys',
+    FAF_TT: 'Faf-tt',
+    FAFIEC: 'Fafiec',
+    FAFIH: 'Fafih',
+    FAFSEA: 'Fafsea',
+    FIF_PL: 'Fif pl',
+    FONGECIF: 'Fongecif',
+    FORCO: 'Forco',
+    INTERGROS: 'Intergros',
+    OPCA3_PLUS: 'Opca3+',
+    OPCAIM: 'Opcaim',
+    OPCALIA: 'Opcalia',
+    OPCALIM: 'Opcalim',
+    OPCA_BAIA: 'Opca baia',
+    OPCA_DEFI: 'Opca defi',
+    OPCA_TRANSPORTS: 'Opca transports',
+    UNIFAF: 'Unifaf',
+    UNIFORMATION: 'Uniformation',
+    VIVEA: 'Vivea',
+    ADDRESS_PRACTICE: 'Adresse de pratique',
+    ADDRESS_HEAD_OFFICE: 'Adresse du siège social',
+    ADDRESS_CONTACT: 'Adresse de contact',
+    ADDRESS_BILL: 'Adresse de facturation',
+    ADDRESS_OTHER: 'Autre adresse',
+    PRINCIPAL: 'Contact principal',
+    BILL: 'Contact de facturation',
+    OTHER: 'Autre',
+    CONTACT: 'Contact'
+  })
+}

+ 135 - 0
lang/field/fr-FR.js

@@ -0,0 +1,135 @@
+export default (context, locale) => {
+  return ({
+    parameters: 'Paramètres',
+    cycle: 'Cycle',
+    timing: 'Durée d\'un enseignement (en minutes)',
+    educationTiming: 'Durée d\'un enseignement (en minutes)',
+    superAdmin: 'Compte super-admin',
+    username: 'Login de connexion',
+    residenceArea: 'Zones de résidence',
+    desactivateOpentalentSiteWeb: 'Désactiver le site opentalent',
+    reactivateOpentalentSiteWeb: 'Réactiver le site Opentalent',
+    passwordSMS: 'Mot de passe SMS',
+    usernameSMS: 'Nom d\'utilisateur SMS',
+    smsSenderName: 'Personnaliser le nom de l\'expéditeur SMS',
+    attendance: 'Absences',
+    sendAttendanceEmail: 'Prévenir automatiquement la famille par mail en cas d\'absence non justifiée',
+    sendAttendanceSms: 'Prévenir automatiquement la famille par sms en cas d\'absence non justifiée',
+    bulletinReceiver: 'Adresser le bulletin à',
+    bulletinEditWithoutEvaluation: 'Editer également les bulletins ne contenant aucune évaluation',
+    bulletinShowAverages: 'Afficher les moyennes',
+    bulletinShowAbsences: 'Afficher les absences',
+    bulletinViewTestResults: 'Afficher les résultats des examens',
+    bulletinShowEducationWithoutEvaluation: 'Afficher les enseignements ne contenant aucune évaluation',
+    bulletinDisplayLevelAcquired: 'Affichage niveau acquis',
+    bulletinSignatureDirector: 'Un cadre « Tampon / Signature » pour l\'administration',
+    bulletinPrintAddress: 'L\'adresse postale de l\'élève ou son tuteur',
+    bulletinWithTeacher: 'Le nom du professeur',
+    bulletin_parameters: 'Bulletins',
+    sms: 'Sms',
+    web_parameters: 'Site internet',
+    averageMax: 'Note maximale pour les notes du suivi pédagogique (entre 1 et 100)',
+    educational_follow_up: 'Suivi pédagogique',
+    advancedEducationNotationType: 'Type de grilles d\'évaluation',
+    educationPeriodicity: 'Périodicité des évaluations',
+    editCriteriaNotationByAdminOnly: 'Autoriser uniquement l\'administration à modifier les critères d\'évaluation',
+    trackingValidation: 'Contrôle et validation du suivi pédagogique par l\'administration',
+    publicationDirectors: 'Directeur(s) de publication',
+    otherWebsite: 'Autre site web',
+    newSubDomain: 'Nouveau sous domaine',
+    yourSubdomains: 'Vos sous-domaines',
+    timezone: 'Fuseau horaire',
+    qrCode: 'QrCode pour la licence',
+    studentsAreAdherents: 'Les élèves sont également adhérents de l\'association',
+    showAdherentList: 'Afficher la liste des adhérents et leurs coordonnées',
+    endCourseDate: 'Date de fin des cours ',
+    startCourseDate: 'Date de début des cours ',
+    generalParams: 'Paramètres généraux',
+    financialDate: 'Début de la saison financière',
+    musicalDate: 'Début de la saison d\'activité',
+    title: 'Titre',
+    link: 'Lien',
+    organizationArticle: 'Coups de projecteur',
+    youtube: 'Lien YouTube',
+    first_subscription: 'Date de première adhésion',
+    bicInvalid: 'Code BIC invalide',
+    ibanInvalid: 'IBAN invalide',
+    invalid_bic: 'Votre BIC est invalide',
+    invalid_iban: 'Votre IBAN est invalide',
+    importAddress: 'Importer l\'adresse d\'une des personnes de votre structure',
+    addressCountry: 'Pays',
+    legalInformation: 'Informations légales',
+    agrements: 'Agréments',
+    salary: 'Salariés',
+    network: 'Informations réseau',
+    communication: 'Communication',
+    legalStatus: 'Statut juridique',
+    siretNumber: 'N° SIRET',
+    apeNumber: 'Code APE',
+    waldecNumber: 'RNA (ancien Waldec)',
+    identifierCmf: 'Matricule CMF',
+    identifierFfec: 'Matricule FFEC',
+    ffecApproval: 'N° agrément FFEC',
+    description: 'Description',
+    typeOfPractices: 'Type de pratiques',
+    otherPractice: 'Autres ensembles (préciser)',
+    principalType: 'Type principal',
+    contact_point: 'Point de contact',
+    name: 'Nom',
+    acronym: 'Sigle',
+    creationDate: 'Date de création',
+    prefectureName: 'Préfecture ou sous-préfecture',
+    prefectureNumber: 'Numéro de déclaration',
+    declarationDate: 'Date de déclaration',
+    tvaNumber: 'TVA Intracommunautaire',
+    schoolCategory: "Catégorie d'école",
+    typeEstablishment: "Type d'établissement",
+    typeEstablishmentDetail: 'Détails du type',
+    youngApproval: 'Jeunesse-éducation populaire',
+    trainingApproval: 'Organisme de formation',
+    otherApproval: 'Si autre, lesquels',
+    collectiveAgreement: 'Nom de la convention collective',
+    opca: "Nom de l'OPCA",
+    icomNumber: 'N° ICOM',
+    urssafNumber: 'N° URSSAF',
+    email: 'E-mail',
+    emailInvalid: 'E-mail invalide',
+    telphone: 'Téléphone',
+    telphoneInvalid: 'Téléphone invalide',
+    faxNumber: 'Fax',
+    faxNumberInvalid: 'Fax invalide',
+    mobilPhone: 'Portable',
+    mobilPhoneInvalid: 'Portable invalide',
+    actions: 'Actions',
+    twitter: 'Lien Twitter',
+    facebook: 'Lien Facebook',
+    instagram: 'Lien Instagram',
+    image: 'Image',
+    portailVisibility: "Répertorier la structure dans l'annuaire du portail Opentalent",
+    pedagogicBudget: 'Budget pédagogique',
+    budget: 'Montant du dernier budget réalisé',
+    isPedagogicIsPrincipalActivity: "L'activité principale de la stucture est « la pédagogie des arts du cirque »",
+    bank_account: 'IBAN',
+    bankName: 'Nom de la banque',
+    bic: 'Code BIC',
+    iban: 'IBAN',
+    holder: 'Titulaire du compte',
+    debitAddress: 'Domiciliation',
+    principal: 'Principal',
+    address_postal: 'Adresses postales',
+    address: 'Adresse',
+    address_postal_type: 'Nature',
+    addressOwner: 'Chez',
+    streetAddress: 'Adresses',
+    streetAddressSecond: 'Adresses suite',
+    streetAddressThird: 'Adresses suite 2',
+    postalCode: 'Code postal',
+    addressCity: 'Ville',
+    country: 'Pays',
+    addresstype: 'Nature',
+    contactpoint_type: 'Type de contact',
+    phoneNumberInvalid: 'Numéro de téléphone invalide',
+    logo: 'Logo',
+    subdomain: 'Sous-domaine',
+  })
+}

+ 28 - 0
lang/form/fr-FR.js

@@ -0,0 +1,28 @@
+export default (context, locale) => {
+  return ({
+    upload_image: 'Sélectionner une image',
+    of: 'de',
+    allResult: 'Tous',
+    itemsPerPage: 'Nombre de résultats par page',
+    autocomplete_research: 'Aucun résultat ne correspond à votre recherche',
+    add: 'Ajouter',
+    save: 'Enregistrer',
+    save_and_back: 'Enregistrer et retour',
+    back: 'Retour',
+    cancel: 'Annuler',
+    delete: 'Supprimer',
+    confirm_to_delete: 'Vous êtes sur le point de supprimer un élément.',
+    saveSuccess: 'Sauvegarde effectuée',
+    deleteSuccess: 'Suppression effectuée',
+    quit_form: 'Quitter le formulaire',
+    save_and_quit: 'Sauvegarder et quitter le formulaire',
+    back_to_form: 'Retourner au formulaire',
+    attention: 'Attention',
+    updateMap: 'Mise à jour de la carte',
+    start_your_research: 'Commencer à écrire pour rechercher...',
+    no_coordinate_corresponding: 'Aucune coordonnées GPS ne correspondent à votre adresse',
+    quit_without_saving_warning: 'Vous souhaitez quitter ce formulaire sans avoir enregistré',
+    please_wait: 'Veuillez patienter',
+    download: 'Télécharger'
+  })
+}

+ 25 - 0
lang/fr-FR.js

@@ -0,0 +1,25 @@
+import layout from '@/lang/layout/fr-FR'
+import enums from '@/lang/enum/fr-FR'
+import fields from '@/lang/field/fr-FR'
+import rulesAndErrors from '@/lang/rulesAndErrors/fr-FR'
+import form from '@/lang/form/fr-FR'
+import breadcrumbs from '@/lang/breadcrumbs/fr-FR'
+import menuKey from '@/lang/menuKey/fr-FR'
+import help from '@/lang/help/fr-FR'
+import contentSubscription from '@/lang/content/subscription/fr-FR'
+import contentParameters from '@/lang/content/parameters/fr-FR'
+
+export default (context, locale) => {
+  return {
+    ...layout(context, locale),
+    ...enums(context, locale),
+    ...fields(context, locale),
+    ...rulesAndErrors(context, locale),
+    ...form(context, locale),
+    ...breadcrumbs(context, locale),
+    ...menuKey(context, locale),
+    ...help(context, locale),
+    ...contentSubscription(context, locale),
+    ...contentParameters(context, locale),
+  }
+}

+ 26 - 0
lang/help/fr-FR.js

@@ -0,0 +1,26 @@
+/**
+ * Translations for the help tooltips
+ *
+ * @param context
+ * @param locale
+ * @returns {{get_more_functionalities_with_version: string, only_for_cmf_members: string, contact_us_at: string, contact_us_for_show_and_demo: string, starting_from_x_eur_ttc_per_month: string, download_order_form: string, for_x_sms: string, for_only_x_eur_ttc_by_month: string, example: string, domain_name: string, and_benefit: string, public_price_x_ttc_a_year: string, product_sheet: string, get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month: string, dummy_domain_name: string, website: string, version_x_up_to_x_students: string, download_cmf_order_form: string, send_sms_from_app_to_your_members: string, freely_try_our_software: string, starting_from_x_eur_ttc_per_ssm: string, yearly_paid_giving_x_eur_ttc_per_year: string, excluding_license_and_training_fees: string, a_suitable_solution_for_your_artistic_school: string, dummy_email_address: string, associated_mail_address: string, switch_to_version: string, or_by_mail_at: string, of_accounts_for_teachers_and_students: string, of_a_complete_website: string}}
+ */
+export default (context, locale) => {
+  return ({
+    bulletinEditWithoutEvaluationHelp:'Dans le cas où la case n\'est pas cochée, s\'il n\'y a aucune évaluation dans le bulletin, alors le bulletin n\'est pas exporté',
+    bulletinShowEducationWithoutEvaluationHelp: 'Dans le cas où la case est cochée, alors on affiche le texte "Aucune évaluation" pour chaque enseignement sans évaluation',
+    type_of_practices_autocomplete: 'Sélectionnez parmi la liste des types de pratiques, une ou plusieurs pratiques correspondant aux activités de votre structure',
+    logo_upload: '<div>Le logo est utilisée: </div>' +
+      `\n<ul>` +
+      '    <li>dans l\'entête des documents que vos exportez</li>' +
+      '    <li>sur le site internet</li>' +
+      '    <li>dans la recherche d\'une structure sur le site Opentalent ou sur le site d\'une fédération si vous êtes membre de la CMF ' +
+      '        (voir <a target="_blank" href="https://fmfaucigny.opentalent.fr/presentation/societes-adherentes">exemple ici</a>)</li>' +
+      '</ul>',
+    communication_image_upload: 'L\'image est utilisé\n' +
+      'dans la description détaillée d\'une structure sur le site Opentalent ou ' +
+      'sur le site d\'une fédération si vous êtes membre de la CMF ' +
+      '(voir <a target="_blank" href="https://fmfaucigny.opentalent.fr/presentation/societes-adherentes">exemple ici</a>)'
+
+  })
+}

+ 222 - 0
lang/layout/fr-FR.js

@@ -0,0 +1,222 @@
+export default (context, locale) => {
+  return ({
+    general_params:'Général',
+    communication_params:'Communication',
+    students_params:'Suivi des étudiants',
+    education_params:'Enseignements',
+    bills_params:'Facturation',
+    secure_params:'Sécurité',
+    back_to_dashboard:'Quittez les paramètres',
+    universal_create_title_access: 'Quel type de contact souhaitez-vous créer ?',
+    universal_create_title_event: 'Que souhaitez-vous ajouter à votre planning ?',
+    universal_create_title_message: 'Que souhaitez-vous envoyer ?',
+    an_adherent: 'Un adhérent',
+    adherent_text_creation_card: 'Ajoutez un membre à votre structure le statut d\'adhérent',
+    a_ca_member: 'Un membre du CA',
+    ca_member_text_creation_card: 'Ajoutez un président, trésorier, secrétaire, membre actif... à votre Conseil d\'Administration',
+    modif_picture:'Modifier l\'image',
+    image_assistant:'Assistant de téléchargement',
+    delete_assistant:'Assistant de suppression',
+    creative_assistant:'Assistant de création',
+    what_do_you_want_to_create:'Que souhaitez-vous créer ?',
+    previous:'Étape précédente',
+    cancel:'Annuler',
+    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',
+    a_correspondence:'Une correspondance',
+    add_an_event_course:'Ajoutez un évenement, un cours, une prestation pédagogique, un examen... à votre planning',
+    an_event:'Un évènement',
+    add_new_person_student:'Ajoutez un nouveau membre parent, élève, professeur, personnel... à votre répertoire',
+    a_person:'Une personne',
+    other_event_text_creation_card:'Comprend entre autres: auditions, concerts, répétitions, spectacles, stages...',
+    educational_services_text_creation_card:'Correspond aux interventions en milieu scolaire, pénitentiaire, ou hospitalier',
+    exam_text_creation_card:'Permet d\'organiser des examens avec la gestion des jurys, des convocations et des résultats',
+    course_text_creation_card:'On associe les élèves à leurs enseignements, puis à leurs cours, qui peut être périodique ou ponctuel',
+    other_event:'Autre événement',
+    educational_services:'Prestations pédagogiques',
+    exam:'Examen',
+    course:'Cours',
+    sms_text_creation_card: 'Les SMS sont disponible sur option, vous devez disposer de suffisament de crédit',
+    letter_text_creation_card: 'Un courrier est imprimé pour être envoyé par la Poste mais peut aussi être envoyé par mail',
+    email_text_creation_card: 'Les emails peuvent également être des newsletters / lettre d\'information',
+    an_sms: 'Un sms',
+    a_letter: 'Un courrier',
+    an_email: 'Un email',
+    another_type_of_contact: 'Un autre type de contact',
+    a_legal_entity: 'Une personne morale',
+    a_member_of_staff: 'Un membre du personnel',
+    a_teacher: 'Un professeur',
+    a_guardian: 'Un tuteur',
+    a_student: 'Un élève',
+    other_contact_text_creation_card: 'Ajoutez un autre type de contact qui n\'a pas été défini précédemment',
+    moral_text_creation_card: 'Ajoutez les structures qui vous soutiennent ou avec qui vous travaillez',
+    personnel_text_creation_card: 'Ajoutez un membre à votre personnel et donnez-lui un accès administratif',
+    teacher_text_creation_card: 'Ajoutez un professeur à votre personnel et donnez-lui un accès pédagogique',
+    student_text_creation_card: 'Inscrivez un nouvel élève via le formulaire de la vue famille',
+    guardian_text_creation_card: 'Ajoutez un tuteur à votre carnet d\'adresses afin de l\'associer ultérieurement à un élève',
+    click_here: 'cliquez ici',
+    super_admin_switch_account: 'Vous utilisez une connexion SWITCH. Afin de retourner sur votre compte veuillez',
+    insurance_cmf_subscription: 'Souscrire un contrat assurance CMF',
+    renew_insurance_cmf: 'Accéder au renouvellement de votre assurance CMF',
+    upload_cotisation_invoice: 'Télécharger la facture de votre appel de cotisation',
+    cotisation_access: 'Accéder au renouvellement de cotisation à ma fédération',
+    information_new_online_registration: 'Nouvelle préinscription',
+    not_production_environment: 'ATTENTION ! Vous êtes sur un environnement {env}.',
+    multi_account_alert: 'Vous êtes connecté, en tant que <strong>{fullname}</strong>, avec un accès famille. Utilisez l\'icône',
+    multi_account_alert_next: 'en haut à droite pour changer les informations des autres membres de votre famille.',
+    not_current_year: 'Votre logiciel est actuellement placé dans une autre année que celle actuelle, et/ou affiche des données passées/futures.',
+    not_current_year_reset: 'Cliquez ici pour afficher les données de l\'année actuelle.',
+    welcome: 'Accueil',
+    address_book: 'Répertoire',
+    person: 'Personnes',
+    family_view: 'Vue famille',
+    education_student_next_year: 'Gestion des inscriptions',
+    commissions: 'Commissions',
+    my_network: 'Répertoire du réseau',
+    network: 'Réseau',
+    schedule: 'Agenda',
+    attendances: 'Absences',
+    equipment: 'Parc matériel',
+    education_state: 'Suivi pédagogique',
+    criteria_notations: "Critères d'évaluation",
+    education_notation_configs: "Grilles d'évaluation",
+    seizure_period: 'Périodes de saisie',
+    test_seizure: 'Saisie des évaluations',
+    test_validation: 'Validation par évaluation',
+    examen_results: 'Résultats des examens',
+    education_by_student_validation: 'Validation par enseignement',
+    billing: 'Facturation',
+    billing_product: 'Produits',
+    billing_products_by_student: 'Produits par élève',
+    billing_edition: 'Édition des factures',
+    billing_accounting: 'Factures et avoirs',
+    billing_payment_list: 'Journal des règlements',
+    pes_export: 'Export JVS',
+    berger_levrault_export: 'Export Berger Levrault',
+    jvs_export: 'Export JVS',
+    inbox: 'Boite d\'envoi',
+    message_send: 'Éléments envoyés',
+    message_templates: 'Modèles',
+    communication: 'Communication',
+    donors: 'Partenariats et dons',
+    medals: 'Médailles',
+    stats: 'Statistiques',
+    report_activity: 'Rapport d\'activité',
+    educations_quotas_by_education: 'Quotas par enseignement',
+    fede_stats: 'Fédérations',
+    structure_stats: 'Structures',
+    rate_cotisation: 'Saisie du tarif',
+    parameters_cotisation: 'Paramètrer l\'appel de cotisation',
+    send_cotisation: 'Appel des cotisations',
+    state_cotisation: 'Suivi des cotisations',
+    pay_cotisation: 'Saisie des règlements',
+    check_cotisation: 'Remise de chèques',
+    ledger_cotisation: 'Journal des règlements',
+    magazine_cotisation: 'Bulletin',
+    ventilated_cotisation: 'Cotisations ventilées par sous-total',
+    pay_erase_cotisation: 'Suppression de règlements',
+    resume_cotisation: 'Etat des transmissions',
+    history_cotisation: 'Historique des cotisations',
+    call_cotisation: 'Règlements à la fédération',
+    history_struture_cotisation: 'Cotisations fédérales',
+    insurance_cotisation: 'Assurance CMF',
+    resume_all_cotisation: 'Toutes les cotisations',
+    resume_pay_cotisation: 'Cotisation avec un règlement reçu ou en attente',
+    cotisations: 'Cotisations',
+    all_accesses: 'Toutes les personnes',
+    admin2ios: 'Administration 2ios',
+    all_organizations: 'Toutes les structures',
+    tips: 'Tips',
+    actions_lead: 'Actions à conduire',
+    renewall_list: 'Structures à relancer',
+    settlements: 'Règlements effectués',
+    pendings_settlements: 'Règlements en attentes',
+    outages_notice: 'Coupure de service',
+    degraded: 'Clients',
+    dgv: 'Assurance CMF',
+    cmf_cotisation: 'Cotisation CMF',
+    right_menu: 'Droits version 5.9',
+    tree_menu: 'Gestion de l\'arbre',
+    website: 'Site internet',
+    advanced_modification: 'Administration site internet',
+    simple_modification: 'Modifications simplifiées',
+    create: 'Créer',
+    help_access: 'Accès aide',
+    configuration: 'Configuration',
+    organization_page: 'Fiche de la structure',
+    cmf_licence_generate: 'Générer la licence CMF de la structure',
+    cmf_structure_licence: "Licence CMF de la structure",
+    your_cmf_licence: "Votre licence CMF",
+    cmf_licence_details_url: "Consulter les avantages de la licence CMF",
+    generate: "Générer",
+    parameters: 'Préférences',
+    place: 'Lieux',
+    education: 'Enseignements',
+    tag: 'Tags',
+    activities: 'Sections',
+    billing_settings: 'Facturation',
+    online_registration_settings: 'Pré-inscription(s) en ligne',
+    transition_next_year: 'Passage à l\'année suivante',
+    course_duplication: 'Dupliquer les cours hebdomadaires',
+    import: 'Importer',
+    schooling_year: 'Année scolaire',
+    season_year: 'Saison',
+    cotisation_year: 'Année de cotisation',
+    multiAccesses: 'Mes structures',
+    familyAccesses: 'Changement d\'utilisateur',
+    display_data: 'Afficher les données',
+    past: 'Passée',
+    present: 'Présent',
+    future: 'Future',
+    notification: 'Notifications',
+    history_help: 'Personnaliser la période d\'affichage',
+    period_choose: 'Période à afficher',
+    date_choose: 'Veuillez sélectionner deux dates',
+    my_list: 'Mes listes',
+    searchList: 'Rechercher parmi mes listes personnalisées',
+    template_systems: 'Mails système',
+    informations: 'Informations',
+    more_features: 'Plus de fonctionnalités',
+    client_id: 'Numéro de client',
+    version: 'Version',
+    services: 'Services',
+    bills: 'Factures',
+    paid: 'Payée',
+    unpaid: 'Impayée',
+    reference: 'Référence',
+    date: 'Date',
+    taxExcludedAmount: 'Montant H.T.',
+    taxIncludedAmount: 'Montant TTC',
+    status: 'Statut',
+    remaining_sms_credit: 'Crédit SMS restant',
+    paying_structure: 'Établissement payeur',
+    no_bill_to_display: 'Aucune facture à afficher',
+    my_account: 'Mon compte',
+    my_schedule_page: "Mon planning",
+    attendance_bookings_menu: "Gestion des absences",
+    my_attendance: "Mes absences",
+    my_invitation: "Mes invitations",
+    my_students: "Mes élèves",
+    my_students_education_students: "Suivi pédagogique",
+    my_education_students: "Mes évaluations",
+    send_an_email: "Envoyer un email",
+    my_documents: "Mes documents",
+    my_profile: "Mon profil",
+    adherent_list: "Liste des adhérents avec leurs coordonnées",
+    my_subscription: "Mon abonnement",
+    my_bills: "Mes factures",
+    print_my_licence: "Imprimer ma licence CMF",
+    logout: "Se déconnecter",
+    all_notification: "Toutes les notifications",
+    your_file: "Votre fichier",
+    is_ready_to_be_downloaded: "est près à être téléchargé",
+    your_message: "Votre message",
+    has_been_sent: "a été envoyé",
+    ready_to_be: "est prêt à être",
+    none: "Aucun",
+    please_confirm: "Veuillez confirmer",
+    yes: "Oui",
+    no: "Non",
+  })
+}

+ 8 - 0
lang/menuKey/fr-FR.js

@@ -0,0 +1,8 @@
+export default (context, locale) => {
+  return ({
+    attendanceListMenuKey: 'Absences',
+    billAndBillCreditListMenuKey: 'Facturation',
+    equipmentListMenuKey: 'Matériel',
+    personRepertoryListMenuKey: 'Répertoire'
+  })
+}

+ 24 - 0
lang/rulesAndErrors/fr-FR.js

@@ -0,0 +1,24 @@
+export default (context, locale) => {
+  return ({
+    value_need_to_be_bigger_than_0: 'La valeur doit être plus grande que 0',
+    forbidden: 'Vous ne possédez pas les droits nécessaires pour effectuer cette opération',
+    wrong_mobyt_credentials: 'Identifiants SMS incorrects',
+    smsSenderName_error: 'Seuls les caractères alphanumériques sont permis, sans espaces, sans accent et sans caractères spéciaux',
+    between_1_and_10: 'La valeur doit être comprise entre 0 et 10',
+    between_0_and_100: 'La valeur doit être comprise entre 0 et 10',
+    invalid_form: 'Formulaire invalide',
+    required: 'Ce champs est obligatoire',
+    name_length_rule: 'La longueur du nom doit être de moins de 128 caractères',
+    siret_error: 'N° de siret non valide',
+    email_error: 'Adresse email invalide',
+    phone_error: 'Numéro de téléphone invalide',
+    ADDRESS_PRACTICE_non_unique: 'Vous ne pouvez pas avoir 2 adresses de pratique',
+    ADDRESS_HEAD_OFFICE_non_unique: 'Vous ne pouvez pas avoir 2 adresses de siège social',
+    ADDRESS_CONTACT_non_unique: 'Vous ne pouvez pas avoir 2 adresses de contact',
+    ADDRESS_BILL_non_unique: 'Vous ne pouvez pas avoir 2 adresses de facturation',
+    PRINCIPAL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact principaux',
+    BILL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact de facturation',
+    CONTACT_non_unique: 'Vous ne pouvez pas avoir 2 points de contact',
+    could_not_contact_server_please_try_again: 'Le serveur n\'a pas pu être contacté, veuillez réessayer un peu plus tard',
+  })
+}

+ 1 - 1
layouts/default.vue

@@ -17,7 +17,7 @@
         <slot />
       </v-main>
 
-<!--      <lazy-LayoutAlertContainer />-->
+      <LazyLayoutAlertContainer />
 
     </v-app>
   </div>

+ 1 - 1
package.json

@@ -65,7 +65,7 @@
     "pinia-orm": "1.0.3",
     "sass": "^1.54.5",
     "uuid": "^9.0.0",
-    "vuetify": "^3.0.0-beta.11",
+    "vuetify": "3.0.0-beta.13",
     "yaml-import": "^2.0.0"
   }
 }

+ 1 - 1
pages/index.vue

@@ -1,6 +1,6 @@
 <template>
   <main>
-    <nuxt-link to="/organization">Goto organizations</nuxt-link>
+    <nuxt-link to="/poc/1">Goto organizations</nuxt-link>
   </main>
 </template>
 

+ 44 - 45
pages/poc/[id].vue

@@ -1,22 +1,21 @@
 <template>
   <main>
-    <p>Edit :{{ file }}</p>
+    <div>
+      <p v-show="pending">Pending...</p>
+      <p>Edit :{{ data }}</p>
 
-    <form @submit.prevent="" class="my-3">
-      <input v-model="file.name" type="text" />
+      <form @submit.prevent="" @change="onFileChange" class="my-3">
+        <v-text-field v-model="data.name" type="text" />
 
-      <select v-model="file.status">
-        <option value="PENDING">Pending</option>
-        <option value="READY">Ready</option>
-        <option value="DELETED">Deleted</option>
-        <option value="ERROR">Error</option>
-      </select>
+        <v-select v-model="data.status" :items="['PENDING', 'READY', 'DELETED', 'ERROR']">
+        </v-select>
 
-      <button @click="cancelAndGoBack">Annuler</button>
-      <button type="submit" value="Enregistrer" @click="save">Enregistrer</button>
-
-      <button type="submit" value="Supprimer" @click="deleteAndGoBack" class="mt-5">Supprimer</button>
-    </form>
+        <v-btn @click="cancel" class="ma-5">Annuler</v-btn>
+        <v-btn @click="save" class="ma-5">Enregistrer</v-btn>
+        <v-btn @click="deleteAndGoBack" class="ma-5">Supprimer</v-btn>
+        <v-btn @click="refresh" class="ma-5">Refresh</v-btn>
+      </form>
+    </div>
   </main>
 </template>
 
@@ -25,63 +24,63 @@ import {useEntityManager} from "~/composables/data/useEntityManager";
 import {ref, Ref} from "@vue/reactivity";
 import ApiResource from "~/models/ApiResource";
 import {File} from "~/models/Core/File";
+import {navigateTo, useAsyncData} from "#app";
 
 const route = useRoute()
 
 const id: Ref<number> = ref(parseInt(route.params.id as string))
+const valid: Ref<boolean> = ref(true)
 
 const em = useEntityManager()
+
+const { data, pending, refresh } =  useAsyncData(
+    'file_' + id.value,
+    () => em.fetch(File, id.value)
+)
 //@ts-ignore
-let file: ApiResource = reactive(await em.fetch(File, id.value))
+const file = reactive(data.value) as ApiResource
+
+const onFileChange = () => {
+  console.log(file)
+  em.getRepository(File).save(file)
+}
+
 
 const save = async () => {
-  console.log('save')
-  //@ts-ignore
   await em.persist(File, file)
-  navigateTo('/poc')
 }
 
-const cancelAndGoBack = async () => {
+const cancel = async () => {
   if (em.isNewEntity(File, id.value)) {
     await em.delete(File, file)
   } else {
     em.reset(File, file)
   }
-  navigateTo('/poc')
 }
 
-const deleteAndGoBack = async () => {
+const deleteItem = async () => {
   await em.delete(File, file)
-  navigateTo('/poc')
 }
 
-</script>
-
-<style>
-a {
-  color: blue;
-  cursor: pointer;
-}
-a:hover {
-  text-decoration: underline;
+const goBack = () => {
+  navigateTo('/poc')
 }
 
-button {
-  border: grey solid 1px;
-  padding: 5px;
-  margin: 5px;
-  cursor: pointer;
-}
-button:hover {
-  text-decoration: underline;
+const saveAndGoBack = async () => {
+  await save()
+  goBack()
 }
-button:focus {
-  background-color: lightgrey;
+
+const cancelAndGoBack = async () => {
+  await cancel()
+  goBack()
 }
 
-form {
-  display: flex;
-  flex-direction: column;
-  max-width: 500px;
+const deleteAndGoBack = async () => {
+  await deleteItem()
+  goBack()
 }
+</script>
+
+<style>
 </style>

+ 22 - 27
pages/poc/index.vue

@@ -1,19 +1,21 @@
 <template>
   <main>
-    <ul>
-      <li v-for="file in files">
-        <nuxt-link :to="'/poc/' + file.id" class="mr-3">{{ file.name }}</nuxt-link>
-      </li>
-    </ul>
-    <div class="ma-3">{{ totalItems }} results</div>
-    <div class="ma-3">
-      <button @click="goToPreviousPage" class="mr-3">Previous page ({{ previousPage }})</button>
-      <span class="mx-3">Page : {{ page }}</span>
-      <button @click="goToNextPage">Next page ({{ nextPage }})</button>
-      <span class="mx-2"> (Last page : {{ lastPage }})</span>
-    </div>
-    <div class="ma-3">
-      <nuxt-link to="/poc/new">Create</nuxt-link>
+    <div>
+      <ul>
+        <li v-for="file in files">
+          <nuxt-link :to="'/poc/' + file.id" class="mr-3">{{ file.name }}</nuxt-link>
+        </li>
+      </ul>
+      <div class="ma-3">{{ totalItems }} results</div>
+      <div class="ma-3">
+        <button @click="goToPreviousPage" class="mr-3">Previous page ({{ pagination.previous }})</button>
+        <span class="mx-3">Page : {{ page }}</span>
+        <button @click="goToNextPage">Next page ({{ pagination.next }})</button>
+        <span class="mx-2"> (Last page : {{ pagination.last }})</span>
+      </div>
+      <div class="ma-3">
+        <nuxt-link to="/poc/new">Create</nuxt-link>
+      </div>
     </div>
   </main>
 </template>
@@ -23,29 +25,22 @@
   import {Ref} from "@vue/reactivity";
   import ApiResource from "~/models/ApiResource";
   import {File} from "~/models/Core/File";
+  import {Pagination} from "~/types/data";
+  import {useReactiveUpdate} from "~/composables/data/useReactiveUpdate";
 
-  const id: Ref<number> = ref(726900)
   const em = useEntityManager()
   let files: Array<ApiResource> = reactive([])
   const page: Ref<number> = ref(1)
 
   const totalItems: Ref<number> = ref(0)
-  const firstPage: Ref<number> = ref(1)
-  const lastPage: Ref<number> = ref(1)
-  const previousPage: Ref<number | null> = ref(null)
-  const nextPage: Ref<number | null> = ref(null)
+  let pagination: Pagination = reactive({first: 1, last: 1, next: undefined, previous: undefined})
 
   const fetchAll = async function() {
     const collection = await em.fetchAll(File, page.value)
 
-    // On met à jour l'array sans la remplacer pour ne pas perdre la réactivité
-    em.reactiveUpdate(files, collection.items)
-
-    totalItems.value = collection.totalItems || 0
-    firstPage.value = collection.firstPage || 1
-    lastPage.value = collection.lastPage || 1
-    previousPage.value = collection.previousPage || null
-    nextPage.value = collection.nextPage || null
+    // On met à jour les arrays sans les remplacer avec useReactiveUpdate pour ne pas perdre la réactivité
+    useReactiveUpdate(files, collection.items)
+    useReactiveUpdate(pagination, collection.pagination)
   }
   await fetchAll()
 

+ 1 - 1
pages/poc/new.vue

@@ -26,7 +26,7 @@ import {File} from "~/models/Core/File";
 
 const em = useEntityManager()
 //@ts-ignore
-let file: ApiResource = reactive(await em.new(File))
+let file: ApiResource = reactive(await em.newInstance(File))
 
 const save = async () => {
   console.log('save')

+ 2 - 1
plugins/vuetify.ts

@@ -6,6 +6,7 @@ export default defineNuxtPlugin(nuxtApp => {
     const vuetify = createVuetify({
         components,
         directives,
+        ssr: true,
     })
     nuxtApp.vueApp.use(vuetify)
-})
+})

+ 18 - 5
services/data/apiRequestService.ts

@@ -3,15 +3,17 @@
  *
  * It will send basic http requests and returns raw results
  */
-import {AssociativeArray, Connector, HTTP_METHOD} from "~/types/data.d";
+import {AssociativeArray, HTTP_METHOD} from "~/types/data.d";
+import {$Fetch} from "nitropack";
+import {FetchOptions} from "ohmyfetch";
 
 class ApiRequestService {
-    private connector: Connector;
+    private readonly fetch: $Fetch
 
     public constructor(
-        connector: Connector,
+        fetch: $Fetch
     ) {
-        this.connector = connector
+        this.fetch = fetch
     }
 
     /**
@@ -91,7 +93,18 @@ class ApiRequestService {
         params: AssociativeArray | null = null,
         query: AssociativeArray | null = null
     ): Promise<Response> {
-        return await this.connector.request(method, url, body, params, query)
+        const config: FetchOptions = { method }
+        if (params) {
+            config.params = params
+        }
+        if (query) {
+            config.query = query
+        }
+        if (method === HTTP_METHOD.POST || method === HTTP_METHOD.PUT) {
+            config.body = body
+        }
+
+        return this.fetch(url, config)
     }
 }
 

+ 0 - 50
services/data/connector/ohMyFetchConnector.ts

@@ -1,50 +0,0 @@
-import {$Fetch} from "nitropack";
-import {FetchOptions} from "ohmyfetch";
-import {AssociativeArray, Connector, HTTP_METHOD} from "~/types/data.d";
-
-/**
- * Connector for the ohmyfetch library
- *
- * @see https://github.com/unjs/ohmyfetch
- */
-class OhMyFetchConnector implements Connector {
-    private readonly fetch: $Fetch
-
-    public constructor(
-        fetcher: $Fetch,
-    ) {
-        this.fetch = fetcher
-    }
-
-    /**
-     * Send an HTTP request
-     *
-     * @param method
-     * @param url
-     * @param body
-     * @param params
-     * @param query
-     */
-    request(
-        method: HTTP_METHOD,
-        url: string,
-        body: string | null = null,
-        params: AssociativeArray | null = null,
-        query: AssociativeArray | null = null
-    ) {
-        const config: FetchOptions = { method }
-        if (params) {
-            config.params = params
-        }
-        if (query) {
-            config.query = query
-        }
-        if (method === HTTP_METHOD.POST || method === HTTP_METHOD.PUT) {
-            config.body = body
-        }
-
-        return this.fetch(url, config)
-    }
-}
-
-export default OhMyFetchConnector

+ 30 - 59
services/data/entityManager.ts

@@ -18,9 +18,11 @@ import {AssociativeArray, Collection} from "~/types/data.d";
 class EntityManager {
     private CLONE_PREFIX = '_clone_'
 
-    private apiRequestService: ApiRequestService;
+    private apiRequestService: ApiRequestService
 
-    public constructor(apiRequestService: ApiRequestService) {
+    public constructor(
+        apiRequestService: ApiRequestService
+    ) {
         this.apiRequestService = apiRequestService
     }
 
@@ -39,56 +41,24 @@ class EntityManager {
      * @param model
      * @param properties
      */
-    public new(model: typeof ApiResource, properties: object = {}) {
+    public newInstance(model: typeof ApiResource, properties: object = {}): ApiResource {
         const repository = this.getRepository(model)
 
-        const entity = repository.make(properties)
+        let entity = repository.make(properties)
 
         // @ts-ignore
         if (!properties.hasOwnProperty('id') || !properties.id) {
             // Object has no id yet, we give him a temporary one
             entity.id = 'tmp' + uuid4()
         }
-        repository.save(entity)
+
+        entity = repository.save(entity)
 
         this.saveInitialState(model, entity)
 
         return entity
     }
 
-    /**
-     * On met à jour directement l'entité par référence ou la liste d'entités,
-     * pour maintenir la réactivité lorsque l'entité ou l'array est déclarée comme réactive
-     *
-     * Attention à ce que le sujet et la nouvelle valeur soient des objets de même type
-     *
-     * @see http://underscorejs.org/#extend
-     * @param subject
-     * @param newValue
-     */
-    public reactiveUpdate(subject: ApiResource | Array<ApiResource>, newValue: ApiResource | Array<ApiResource>) {
-        if (typeof subject !== typeof newValue) { // TODO: remplacer par des règles typescript
-            console.log('Error : subject and new value have to share the same type')
-            return
-        }
-        if (Array.isArray(subject)) {
-            this.reactiveUpdateArray(subject as Array<ApiResource>, newValue as Array<ApiResource>)
-        } else {
-            this.reactiveUpdateItem(subject as ApiResource, newValue as ApiResource)
-        }
-    }
-
-    private reactiveUpdateItem(entity: ApiResource, newEntity: ApiResource) {
-        useExtend(entity, newEntity)
-    }
-
-    private reactiveUpdateArray(items: Array<ApiResource>, newItems: Array<ApiResource>) {
-        items.length = 0
-        newItems.forEach((f: ApiResource) => {
-            items.push(f)
-        })
-    }
-
     /**
      * Fetch an Entity / ApiResource by its id, save it to the store and returns it
      *
@@ -117,40 +87,41 @@ class EntityManager {
         // deserialize the response
         const attributes = HydraDenormalizer.denormalize(response).data as object
 
-        return this.new(model, attributes)
+        return this.newInstance(model, attributes)
     }
 
-    public findBy(model: typeof ApiResource, query: AssociativeArray) {
-        // TODO: implement
-    }
-
-    public async fetchAll(model: typeof ApiResource, page: number = 1): Promise<Collection> {
+    public async fetchBy(model: typeof ApiResource, query: AssociativeArray, page: number = 1): Promise<Collection> {
         let url = Url.join('api', model.entity)
 
         if (page !== 1) {
-            url = Url.join(url, '?page=' + page)
+            query['page'] = page
         }
-        console.log(url)
 
-        const response = await this.apiRequestService.get(url)
+        const response = await this.apiRequestService.get(url, query)
 
         // deserialize the response
-        const collection = HydraDenormalizer.denormalize(response)
+        const apiCollection = HydraDenormalizer.denormalize(response)
 
-        const items = collection.data.map((attributes: object) => {
-            return this.new(model, attributes)
+        const items = apiCollection.data.map((attributes: object) => {
+            return this.newInstance(model, attributes)
         })
 
         return {
             items,
-            totalItems: collection.metadata.totalItems,
-            firstPage: collection.metadata.firstPage,
-            lastPage: collection.metadata.lastPage,
-            nextPage: collection.metadata.nextPage,
-            previousPage: collection.metadata.previousPage,
+            totalItems: apiCollection.metadata.totalItems,
+            pagination: {
+                first: apiCollection.metadata.firstPage || 1,
+                last: apiCollection.metadata.lastPage || 1,
+                next: apiCollection.metadata.nextPage || undefined,
+                previous: apiCollection.metadata.previousPage || undefined,
+            }
         }
     }
 
+    public async fetchAll(model: typeof ApiResource, page: number = 1): Promise<Collection> {
+        return this.fetchBy(model, [], page)
+    }
+
     /**
      * Persist the entity as it is in the store into the data source via the API
      *
@@ -174,7 +145,7 @@ class EntityManager {
         }
 
         const hydraResponse = await HydraDenormalizer.denormalize(response)
-        const returnedEntity = this.new(model, hydraResponse.data)
+        const returnedEntity = this.newInstance(model, hydraResponse.data)
 
         this.saveInitialState(model, returnedEntity)
 
@@ -185,7 +156,7 @@ class EntityManager {
             await this.refreshProfile()
         }
 
-        this.reactiveUpdate(entity, returnedEntity)
+        return returnedEntity
     }
 
     /**
@@ -222,7 +193,7 @@ class EntityManager {
         const repository = this.getRepository(model)
         repository.save(initialEntity)
 
-        this.reactiveUpdate(entity, initialEntity)
+        return initialEntity
     }
 
     /**
@@ -234,7 +205,7 @@ class EntityManager {
         // deserialize the response
         const hydraResponse = await HydraDenormalizer.denormalize(response)
 
-        const profile = this.new(MyProfile, hydraResponse.data)
+        const profile = this.newInstance(MyProfile, hydraResponse.data)
 
         const profileAccessStore = useProfileAccessStore()
         profileAccessStore.setProfile(profile)

+ 1 - 1
services/data/serializer/denormalizer/yamlDenormalizer.ts

@@ -1,6 +1,6 @@
 import { read } from 'yaml-import'
-import { AnyJson } from '~/types/interfaces'
 import {dump, load} from 'js-yaml';
+import {AnyJson} from "~/types/data";
 
 
 /**

+ 12 - 12
services/utils/objectProperties.ts → services/utils/objectUtils.ts

@@ -1,11 +1,13 @@
 /**
  * @category Services/utils
- * @class ObjectProperties
+ * @class ObjectUtils
  * Classe aidant à manipuler des Objets
  */
 import {AnyJson} from "~/types/data";
+import ApiResource from "~/models/ApiResource";
 
-class ObjectProperties {
+
+export default class ObjectUtils {
   /**
    * Flatten un objet nested en un objet avec un seul niveau avec des noms de propriétés transformées comme cela 'foo.bar'
    * L'objet passé en paramètre reste inchangé car il est cloné
@@ -19,7 +21,7 @@ class ObjectProperties {
     if (typeof object !== 'object') {
       throw new TypeError('Expecting an object parameter')
     }
-    return Object.keys(object).reduce(
+    return ObjectUtils.keys(object).reduce(
       (values: AnyJson, name: string) => {
         if (!object.hasOwnProperty(name)) {
           return values
@@ -28,7 +30,7 @@ class ObjectProperties {
         if (this.isObject(object[name])) {
           if (!excludedProperties.includes(name)) {
             const flatObject = this.cloneAndFlatten(object[name])
-            Object.keys(flatObject).forEach((flatObjectKey) => {
+            ObjectUtils.keys(flatObject).forEach((flatObjectKey) => {
               if (!flatObject.hasOwnProperty(flatObjectKey)) { return }
               values[name + '.' + flatObjectKey] = flatObject[flatObjectKey]
             })
@@ -53,7 +55,7 @@ class ObjectProperties {
     if (typeof object !== 'object') {
       throw new TypeError('Expecting an object parameter')
     }
-    return Object.keys(object).reduce((values, name) => {
+    return ObjectUtils.keys(object).reduce((values, name) => {
       if (!object.hasOwnProperty(name)) {
         return values
       }
@@ -81,16 +83,16 @@ class ObjectProperties {
     return value !== null &&
       typeof value === 'object' &&
       !Array.isArray(value) &&
-      Object.prototype.toString.call(value) !== '[object Date]'
+      ObjectUtils.prototype.toString.call(value) !== '[object Date]'
   }
 
   /**
    * Clône l'objet et ses propriétés.
-   * @param {Object} object
-   * @return {Object}
+   * @param {ObjectUtils} object
+   * @return {ObjectUtils}
    */
   clone (object: AnyJson): AnyJson {
-    return Object.keys(object).reduce((values: AnyJson, name: string) => {
+    return ObjectUtils.keys(object).reduce((values: AnyJson, name: string) => {
       if (object.hasOwnProperty(name)) {
         values[name] = object[name]
       }
@@ -107,7 +109,7 @@ class ObjectProperties {
     if (typeof toSort !== 'object') {
       throw new TypeError('Expecting an object parameter')
     }
-    return Object.keys(toSort).sort().reduce(
+    return ObjectUtils.keys(toSort).sort().reduce(
       (obj:any, key:string) => {
         obj[key] = toSort[key]
         return obj
@@ -116,5 +118,3 @@ class ObjectProperties {
     )
   }
 }
-
-export const $objectProperties = new ObjectProperties()

+ 2 - 2
store/page.ts

@@ -8,10 +8,10 @@ export const usePageStore = defineStore('page', {
     }
   },
   actions: {
-    removeSlowlyAlert (context: any) {
+    removeSlowlyAlert () {
       setTimeout(() => {
         this.alerts.shift()
       }, 300)
     }
   }
-})
+})

+ 9 - 4
types/data.d.ts

@@ -47,13 +47,18 @@ interface ApiCollection extends ApiResponse {
     metadata: HydraMetadata
 }
 
+
+interface Pagination {
+    first?: number
+    last?: number
+    next?: number
+    previous?: number
+}
+
 interface Collection {
     items: Array<ApiResource>
+    pagination: Pagination
     totalItems: number | undefined
-    firstPage: number | undefined
-    lastPage: number | undefined
-    nextPage: number | undefined
-    previousPage: number | undefined
 }