Préambule

L'objectif principal de ce TP est d'implémenter une pseudo application bancaire et de compléter l'application générale DrMad, afin de mettre en oeuvre le principe des slots. Il y a donc un scénario à deux niveaux.

Le scénario principal est le suivant :

  • Quand l'utilisateur se trouve sur la page principale de l'application DrMad, il apparaît :
    • en haut, une barre de navigation avec deux boutons : "Boutique" et "Banque"
    • au centre, un texte du genre "Bienvenue DrMad"
  • Quand on clique sur "Boutique", le centre est remplacé par la page d'accueil de la boutique,
  • Quand on clique sur "Banque", le centre est remplacé par la page d'accueil de la banque,

 

Le scénario pour utiliser la banque est le suivant :

  • Quand l'utilisateur se trouve sur la page d’accueil de la banque il voit apparaître :
    • en haut, une barre de navigation avec un seul bouton : "Mon compte" 
    • à gauche, un menu avec 3 boutons les uns sous les autres : "Solde", "Débit/virement", "Relevé". Ces boutons ne sont actifs que si l'utilisateur  a fourni un n° de compte valide via la page "mon compte"
    • au centre, un texte du genre "bienvenue à la banque"
  • Quand on clique sur "Mon compte", le centre est remplacé par une page où il est possible de fournir un n° de compte bancaire, avec un bouton pour valider.
  • S'il correspond à un n° valide, alors on considère que l'on récupère les informations liées au compte (cf. le tableau bankaccounts dans datasource/data.js), que l'on stocke dans le store. Ces informations représentent "le compte courant" dans la suite de l'énoncé. De plus, les boutons du menu doivent devenir actifs et on fait apparaître la page qui donne le solde du compte.

 

  • Quand on clique sur "Solde", le centre est remplacé par une page où le solde du compte courant apparaît, s'il existe  dans le store.

 

  • Quand on clique sur "Débit/virement", le centre est remplacé par une page avec :
    • un champ de saisie permettant de taper une somme,
    • une case à cocher avec un label "Destinataire". Quand on coche cette case, un champ de saisie doit apparaître afin de taper un n° de compte qui sera le destinataire de la somme
    • un bouton pour valider. Un clic sur ce bouton permet de vérifier si la transaction est valide, pour ensuite la créer.
    • Si la vérification échoue, un message d'erreur est affiché dans une boîte de dialogue, sinon, l'uuid de la transaction est affiché.

 

  • Quand on clique sur "Relevé", le centre est remplacé par une page où la liste de toutes les transactions concernant le compte courant sont listées, ce qui inclut celles où le compte est débité mais aussi celles où il apparaît comme destinataire.
  • La liste peut être filtrée avec un intervalle de dates.

 

0°/ Mise en place

La structuration générale reste la même que pour les TPs précédents. De plus, certains composants vont être réutilisés. La mise en place initiale la plus simple consiste donc à :

  • créer un nouveau projet avec vue-cli,
  • remplacer le répertoire src de ce nouveau projet, par celui du TP 4.

 

1°/ Modification des services

Les fonctionnalités impliquées par les scénarios donnés ci-dessus nécessitent d'implémenter et/ou modifier des fonctions de services par rapport aux TPs précédents. En l'occurrence, les fonctions liées à la banque ont été placées dans services/bankaccount.service.js.  et elles récupèrent des données dans la source local, grâce aux fonctions se trouvant dans datasource/controller.js. Ce sont ces 2 fichiers qu'il faut modifier, en partant d'hypothèses sur les requêtes possibles et le format de leur réponse.

Comme pour les services de la boutique, on doit ajouter à controller.js des fonctions qui retournent un résultat avec une structure qui doit être identique à celle retournée par l'API. Pour rappel, cette structure est du type {error: num_erreur, status: ..., data: ...}. S'il y a une erreur, data contient le message d'erreur. Sinon, data contient un objet dont la structure dépend du type de requête.

Voici la liste des fonctions à définir dans controller.js pour gérer la banque à partir de la source de données locale, avec le descriptif de ce qu'elles retournent :

  • getAccount(number) : permet de récupérer toutes les informations d'un compte si number correspond à un numéro valide dans le tableau bankaccounts, importé du fichier datasource/data.js. Si c'est le cas, le champ data de la réponse contient l'objet correspondant dans le tableau. Sinon, data contient un message du type "numéro de compte invalide"
  • getTransactions(id_account) : permet de récupérer toutes les transactions liées à un id de compte (et pas son n°). Pour cela, il faut vérifier dans les objets du tableau transactions si id_account apparaît  dans le champ account ou bien destination . S'il existe de tels objets, le champ data de la réponse contient un tableau de ces objets. Sinon, data contient un message du type "aucune transaction pour ce compte".
  • createWithdraw(id_account, amount) : permet de créer un objet transaction dans le tableau transactions, avec un montant négatif, correspondant à un retrait, donc sans destination et enfin, de débiter le compte de cette somme. Si id_account est valide, le champ data de la réponse contient un objet avec comme structure { uuid: ..., amount: ...}. Le champ uuid a la même valeur que celle du champ uuid de la nouvelle transaction. Le champ amount contient le nouveau solde disponible du compte. Si id_account n'est pas valide, data contient un message du type "id de compte invalide". ATTENTION ! si cette fonction est appelée avec succès par le store, il ne faut pas oublier ensuite de modifier le solde disponible du compte courant.
  • createPayment(id_account, amount, destination) : idem que la fonction précédente mais avec une transaction dont le champ destination est défini, ce qui permet de créditer le compte destinataire s'il existe. Si id_account n'est pas valide, data contient un message du type "id de compte invalide". Si destination n'est pas valide, data contient un message du type "compte destinataire inexistant"

 

Pour modifier bankaccount.service.js, il suffit de reproduire le principe des premiers TPs, en ajoutant une paire de fonctions pour chaque requête :

  • La fonction principale, qui sera appelée par les actions du store, se contente d'appeler la fonction secondaire, et retourne son résultat,
  • La fonction secondaire, qui appelle une des fonctions de controller.js, et retourne son résultat.

Remarque : il est conseillé d'utiliser les règles de nommage des fonctions principales et secondaires déjà utilisées lors des TPs précédents.

Exemple pour récupérer les informations d'un compte : la fonction principale getAccount(number) appelle la fonction secondaire getAccountFromLocalSource(number), qui elle-même appelle la fonction getAccount(number) de controller.js.

 

2°/ Modification du store

Dans le TP 3, le store a déjà été modularisé avec un fichier bank.js qui représente le module concernant la banque. Cependant, ce qui a été mis en place dans les TPs précédents n'est pas totalement adapté au sujet présent. Il faut donc modifier ce module afin de prendre en compte  les fonctionnalités demandées.

A noter que les actions de ce module doivent, comme pour la boutique, faire appel à des fonctions de services, en l'occurrence celles définies dans bankaccount.service.js. et décrites dans la section précédente.

 

3°/ La barre de navigation

 L'objectif est de modifier NavBar.vue pour en faire un composant utilisant les props et les scoped-slot pour le rendre facilement paramétrable par le composant parent. De plus, NavBar n'envoie plus un signal au parent pour signaler qu'un des boutons a été cliqué : il suit directement une route de vue-router donnée via une props.

  • la props titles est renommée en links et devient un tableau contenant des objets au format : { label: ..., to: ...}. La valeur de label est un texte qui s'affiche par défaut dans un bouton et to est une route pour vue-router. : [ label: "boutique", to: "/shop" ]
  • dans le <template>, au lieu d'utiliser directement le label pour chacun des boutons, on définit un scoped-slot à l'intérieur de la balise <button>
  • Par défaut le slot affiche le label, mais en tant que scoped-slot, il doit donner l'accès au label au composant parent, afin que ce dernier puisse customiser son aspect graphique.

On obtient quelque chose du genre :

<button v-for="(link, index) in links" :key="index" ... >
  <slot name="nav-button" :label="link.label" >{{link.label}}</slot>
</button>

 

  • Il faut ensuite ajouter la gestion des clics sur les boutons :
    • en cas de clic, on appelle une fonction goTo(dest), définie dans methods, avec comme paramètre dest la valeur de link.to.
    • la fonction goTo() suit la route donnée en paramètre.

 

Pour mettre en place la barre de navigation principale, il faut modifier App.vue pour indiquer à NavBar.vue le contenu du slot pour chaque bouton :

  • passer comme valeur de la props links le tableau [ { label: "boutique", to: "/shop"}, { label: "banque", to: "/bank"} ],
  • pour le premier bouton, donner comme contenu au slot le mot "boutique" écrit en gras
  • pour le deuxième bouton, donner comme contenu au slot une icône ressemblant à une banque.

Remarques :

  • dans une application vue, les ressources de type icônes se trouvent généralement dans le sous-répertoire assets.
  • une fois placées dans ce répertoire, on peut facilement y faire référence via un chemin d'accès

Exemple :

<template>
  <div>
    <img src="/@/assets/myicon.png">
    ...
  </div>
</template>

 

4°/ Un menu vertical

  • Dans le répertoire components, créer un composant VerticalMenu.vue. 
  • Ce composant fait la même chose que NavBar.vue, excepté que la présentation des boutons est verticale afin de ressembler à un menu et que l'on peut intercaler des "titres" entre les boutons.
  • Pour ce faire, le composant reçoit une props nommée items, qui est un tableau contenant des objets avec la structure { type: "...", label: "...", to: "..." }
  • Le champ type contient comme valeur "title" ou bien "link".
    • dans le premier cas, le composant doit juste afficher un scoped-slot nommé menu-title, dont le contenu par défaut est la valeur de label. Ce slot donne au parent l'accès à label.
    • dans le second cas, le composant affiche une balise <span> contenant elle-meme un scoped-slot nommé menu-link. dont le contenu par défaut est un bouton dont le texte est la valeur de label. Ce slot donne au parent l'accès à label. Un clic sur la balise <span> (ou son contenu) permet de suivre la route indiquée par le champ to.

 

5°/ Le composant racine de la banque

  • Dans le répertoire views, créer un composant BankView.vue. 
  • Ce composant affiche :
    • en haut une barre de navigation avec un seul bouton,
    • à gauche, un menu vertical,
    • au centre, un des composants décrit dans la suite, grâce à une balise <router-view> dont le nom est bankmain.
  • Pour la barre de navigation, on utilise le composant NavBar, auquel on donne en props le tableau [ {link: "Mon compte", to: "/bank/account"} ]
  • Pour le menu, on utilise le composant VerticalMenu, auquel on donne en props le tableau

[ {type: "title", label: "Opérations"}, {type: "button", label: "Solde", to: "/bank/amount"}, {type: "button", label: "Débit/Virement", to: "/bank/operation"}, {type: "title", label: "États"} , {type: "button", label: "Historique", to: "/bank/history"}  ]

  • On ne définit que le contenu du scoped-slot menu-title : on utilise label pour créer du HTML qui affiche la valeur de label en gras, souligné.
  • Pour le scoped-slot menu-link, on garde le comportement par défaut, à savoir afficher un bouton.
 
  • Grâce à <router-view>, le composant affiche différents composants grâce à une route à 2 niveaux. 
  • Il faut donc modifiez router/index.js en conséquence.
  • la route /bank est la route racine. Elle doit afficher le composant BankView (NB : grâce a <router-view> se trouvant dans App.vue) et elle a 5 enfants, qui vont tous s'afficher dans l'emplacement bankmain.
  • la route /bank/home doit permettre d'afficher BankHome.vue. Elle a comme alias /bank 
  • la route /bank/account doit permettre d'afficher BankAccount.vue
  • la route /bank/amount doit permettre d'afficher BankAmount.vue.
  • la route /bank/operation doit permettre d'afficher BankOperation.vue.
  • la route /bank/history doit permettre d'afficher BankHistory.vue

 

6°/ L'accueil de la banque

  • Dans le répertoire views, créer un composant BankHome.vue.
  • Ce composant affiche juste un message de bienvenue à la banque.

 

7°/ Afficher le solde

  • Dans le répertoire views, créer un composant BankAmount.vue.
  • Ce composant affiche juste le solde du compte courant .
  • L'aspect du texte affiché doit être paramétrable par le composant parent via un scoped-slot nommé account-amount.
  • Dans le cas présent, c'est BankView qui doit donner à son fils du HTML avec le solde disponible, suivi du symbole €, en rouge si le solde est négatif, en vert si le solde est positif, le tout dans un champ de saisie non éditable.

 

8°/ Débit & virements

  • Dans le répertoire views, créer un composant BankOperation.vue.
  • Ce composant affiche :
    • un titre, qui est par défaut "Débit / Virement", mais qui peut être changé via un slot.
    • un champ de saisie pour un montant,
    • une case à cocher avec comme label "Destinataire", suivie d'un champ de saisie qui n'apparaît que si la case est cochée.
    • un bouton "Valider".
  • Le fonctionnement du composant doit simplement suivre le comportement donné dans le scénario, sachant que :
    • si l'opération est valide, un texte est affiché sous le bouton "valider", du type : "L'opération est validée avec le n° : xxx-xxx-xxx. Vous pouvez la retrouver dans l'historique". Le n° est l'uuid renvoyé dans la réponse du service.
    • sinon, une boîte de dialogue apparaît avec un message d'erreur.

 

9°/ Un tableau dynamique avec case à cocher

 L'objectif est de faire un composant DataTable.vue, utilisant les props et les scoped-slot pour le rendre facilement paramétrable par le composant parent. Ce composant permet d'afficher :

  • une table avec X, X+1 ou X+2 colonnes et Y+1 lignes, sachant que :
    • la première ligne sert à afficher les entêtes de colonne,
    • les données à affichées sont passées via une props items, qui contient Y objets avec X champs, l'objectif étant d'afficher la valeur de chaque champ sur une même ligne de la table. La structure de items est décrite plus loin.
    • si la props itemCheck vaut true, la première colonne doit afficher des cases à cocher, et les X suivantes, la valeur des champs des objets reçus via la props items. Sinon, on commence chaque ligne avec directement les champs des objets.
    • si la props itemButton vaut true, il faut ajouter une colonne à droite, contenant des boutons.
    • selon la valeurs des 2 props précédentes, on a donc bien entre X et X+2 colonnes à afficher.
    • la props headers contient des objets décrivant les entêtes de chaque colonne, excepté celles des cases à cocher et boutons, qui n'ont pas d'entête. La structure de headers est décrite plus loin.
  • un éventuel bouton après la table, qui apparaît ou non selon la valeur de la props tableButton.

Pour qu'un composant parent fournisse des données à DataTable, il doit respecter une structure stricte concernant les props headers et items :

  • headers est de type tableau d'objet. Chaque objet est associé à une des X colonnes, avec comme format { label: "...", name: "..."}. label est le texte qui doit être affiché dans l'entête d'une colonne, et name est son nom, à savoir celui qui est utilisé comme nom de champ dans la props items.
  • items est de type tableau d'objets, chaque objet contenant les données à afficher sur une même ligne de la table, avec comme format { nom_colonne1: val1, nom_colonne2: val2, ...}. Le nom des champs doit être le même que celui indiqué dans les champs name de headers.

Par ailleurs, les clics sur :

  • les cases à cocher doivent envoyer un événement au parent, nommé itemCheckChanged, avec comme valeur un entier index, qui est le n° de la ligne où la case a été cliquée. Cela permet au parent, si besoin, de maintenir en mémoire un tableau indiquant quelle case est cochée ou non. DataTable ne sait donc pas lui-même si la case est cochée ou non.
  • les boutons doivent envoyer un événement au parent, nommé itemClicked, avec comme valeur un entier index, qui est le n° de la ligne où le bouton a été cliqué.
  • le bouton d'après table doit envoyer un événement au parent, nommé tableClicked, sans valeur.

 

Exemple d'utilisation dans un composant parent qui veut afficher une table avec 4 colonnes (1 pour les cases à cocher, 2 pour les champs de chaque item, 1 pour les boutons), ainsi qu'un bouton après la table. Il y a 4 lignes (1 pour l'entête, 3 pour les données). A noter que dans cet exemple, on ne capture aucun des événements :

<template>
  ...
  <DataTable :headers="headers" :items="items" itemCheck itemButton tableButton></DataTable>
  ...
</template>
<script>
  ...
  data: () => ({
    headers : [ {label: "nom", name: "virus"}, { label: "prix", name: "price} ],
    items: [
      { virus: "grippe", price: 1000 },
      { virus: "covid", price: 150 },
      { virus: "cholera", price: 6666 },
    ]
  })
  ...
</script>

 

10°/ L'historique des transactions

  • Dans le répertoire views, créer un composant BankHistory.vue.
  • Ce composant affiche :
    • un titre, qui est par défaut "Opérations passées", mais qui peut être changé via un slot
    • une case à cocher avec comme label "Filtrer par période", suivie de deux champs de saisie qui n'apparaissent que si la case est cochée, avec comme labels respectifs "Du", "Au".
    • un composant DataTable, contenant des transactions associées au compte courant. Par défaut, elles sont toutes affichées sauf si le filtrage est actif.

 

  • Le comportement du filtrage est le suivant :
    • si seul le premier champ est rempli, on sélectionne toutes les transactions à partir de cette date,
    • si seul le deuxième champ est rempli, on sélectionne toutes les transactions jusqu'à cette date,
    • si les deux champs sont remplis, on sélectionne les transactions entre les deux dates.
    • si le premier champ est déjà rempli, et que l'on tape une date antérieure dans le second champ, ce dernier est vidé puisque invalide.
    • si le deuxième champ est déjà rempli, et que l'on tape une date postérieure dans le premier champ, ce dernier est vidé puisque invalide.
    • dès que l'on valide la saisie d'un des champs, le filtrage doit se faire et donc impacter l'affichage de la table.

 

  • La table doit afficher :
    • 1 colonne avec des cases à cocher,
    • 1 colonne avec le montant des transactions,
    • 1 colonne  avec la date des transactions,
    • 1 colonne avec une lettre, qui doit être S si le compte courant est la source de la transaction et D si elle est la destination.
    • 1 colonne avec des boutons, dont le label est "Voir"
    • 1 bouton après la table avec comme intitulé "Voir".

 

  • Quand une des boutons d'item est cliqué, une boite de dialogue s'ouvre avec dedans l'uuid de la transactions.
  • Quand le bouton d'après table est cliqué, une boite de dialogue s'ouvre avec dedans l'uuid de toutes les transactions dont la case est cochée.