Préambule

L'objectif principal de ce TP est d'implémenter une pseudo boutique en ligne, afin de mettre en oeuvre de façon plus poussée les principes de vuex et de vue-router.  Le scénario à implémenter est le suivant :

  • Quand l'utilisateur se trouve sur la page principale de la boutique il voit apparaître :
    • en haut, une barre de navigation avec un seul bouton : "Login" 
    • au centre, un texte du genre "bienvenue à la boutique"

NB : pour simplifier l'exercice et notamment la gestion du panier, on part du principe que des boutons ne vont apparaître que si l'utilisateur est authentifié via la page de login.

 

  • Quand on clique sur "Login", le centre est remplacé par une page où il est possible de fournir un login/mot de passe pour la boutique.
  • S'ils correspondent à un utilisateur valide, alors on considère que l'on récupère les informations liées à l'utilisateur, que l'on stocke dans le store, comme dans les TPs précédents. De plus :
    • le texte du bouton doit changer pour indiquer "Logout".
    • les boutons "Acheter", "Payer", et "Mes commandes" doivent apparaître.

 

  • Quand on clique sur "Acheter", le centre est remplacé par une page en 2 parties :
    • A gauche, une liste apparaît avec chaque nom de virus suivi d'un champ de saisie numérique permettant d'indiquer le nombre d'exemplaires voulus, et un bouton permettant de mettre le virus dans le panier. Il est également possible de sélectionner plusieurs virus et de tous les mettre dans le panier en une seule fois, grâce à un clic sur un bouton en bas de la liste. Dans les deux cas, les articles choisis et leur nombre sont stockés dans le store, dans les informations de l'utilisateur.
    • A droite, une liste apparaît à droite récapitulant les virus choisis se trouvant dans le panier, ainsi que le montant total (déductions de promotions comprises). En bas du panier,,il est possible de cliquer sur un bouton "Acheter" pour créer une commande, dans l'état "waiting_payment". Une boite de dialogue apparaît faisant apparaître l'uuid de la commande.

 

  • Quand on clique sur "Payer", le centre est remplacé par une page où il est possible de saisir deux uuids : celui de la commande et celui d'une transaction bancaire. Quand on clique sur un bouton "Vérifier", on vérifie :
    • s'il existe une transaction bancaire avec cet uuid,
    • si le montant correspond au montant de la commande,
    • si le destinataire est bien le n° de compte de la boutique.
  • Si tout est ok, la commande passe en état "finalized".

 

  • Quand on clique sur "Mes commandes", le centre est remplacé par une page où il est possible de voir l'historique des commandes passées/en cours. Pour celles qui sont en état "waiting_payment", un bouton permet de l'annuler, et un autre permet de la payer. Un clic sur ce dernier redirige vers la page affichée lorsque l'on clique sur "Payer" dans la barre de navigation, mais avec le champ id de commande déjà rempli.

 

1°/ 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 3.

 

2°/ Le composant racine de la boutique

  • Dans le répertoire views, créer un composant ShopView.vue, avec un template du style :
<template>
  <div>
    <h1>Boutique</h1>
    <router-view name="shopmain"></router-view>
  </div>
</template>
  • Grâce à <router-view>, le composant va pouvoir afficher les différents composants pour gérer la boutique, grâce à une route à 2 niveaux. 
  • Il faut donc modifiez router/index.js en conséquence.
  • la route /shop est la route racine. Elle doit afficher le composant ShopView (NB : grâce a <router-view> se trouvant dans App.vue) et elle a 5 enfants, qui vont tous s'afficher dans l'emplacement shopmain.
  • la route /shop/home doit permettre d'afficher ShopHome.vue. Elle a comme alias /shop (cf. https://router.vuejs.org/guide/essentials/redirect-and-alias.html pour voir des exemples d'alias)
  • la route /shop/login doit permettre d'afficher ShopLogin.vue.
  • la route /shop/buy doit permettre d'afficher ShopBuy.vue
  • la route /shop/pay/:orderId doit permettre d'afficher ShopPay.vue. Le paramètre de cette route orderId doit être transformé en un props pour ShopPay. (cf. section 6°)
  • la route /shop/orders doit permettre d'afficher ShopOrders.vue

 

3°/ L'accueil

  • Dans le répertoire views, créer un composant ShopHome.vue.
  • L'objectif de ce composant est juste d'afficher un texte de bienvenue à la boutique.

 

4°/ Authentification

Cette partie a déjà été implémentée dans les TPs précédents. Il suffit donc de reprendre le composant déjà écrit., et en faisant quelques modifications

La première concerne le fichier localsource.service.js. En effet, dans la solution proposée, la fonction qui recherche un utilisateur dans le fichier source de données data.js se contente de vérifier le login mais pas le mot de passe : il faut également faire cette vérification. Pour ce faire, vous devez :

  • installer le module bcryptjs : npm install bcryptjs
  • importer ce module dans localsource.service.js
  • modifier la fonction de contrôle shopLogin() pour trouver un utilisateur dont le login ET le password correspondent, en appelant bcrypt.compareSync(...) pour vérifier si le mot de passe fournit correspond à celui de l'utilisateur (cf. doc bcryptjs pour son utilisation)

Remarques :

  • l'utilisateur "dr mad" (cf. data.js) a comme login/mdp : drmad / drmad
  • il est possible que vous ayez un warning lors de la compilation de votre application. Normalement, cela n'a aucune influence sur le résultat.

La deuxième consiste à afficher un dialogue d'erreur si le login/mot de passe n'est pas valide.

Enfin, si l’authentification est correcte, on doit suivre la route /shop/buy.

 

5°/ Acheter des items

L'objectif est de permettre de sélectionner des virus et de les ajouter à un "panier d'achat", en plus ou moins grand nombre. (NB : cela impose de modifier les composant CheckedList.vue créé dans le TP précédent.)

5.1°/ Le composant racine des achats

  • Dans le répertoire views, créer un composant ShopBuy.vue.
  • Ce composant se contente d'afficher à gauche un composant ItemsList.vue et à droite BasketList.vue

5.2°/ Les items à acheter

  • Modifier le composant CheckedList.vue afin que :
    • il ait une props supplémentaire nommée itemAmount de type booléen.
    • un champ de saisie numérique soit ajouté devant chaque bouton se trouvant après un item (cf. <input> type number), si itemAmount vaut true.
    • quand on clique sur ce bouton, l'événement envoyé doit contenir l'indice de l'item ET la valeur du champ numérique si celui-ci est visible
    • quand on clique sur le bouton se trouvant après la liste des items, l'événement envoyé doit contenir un tableau avec des couples indice/valeur champ numérique (si le champ est visible) pour chaque item sélectionné. Cela doit entraîner (dans le composant parent) la desélection les items.

 

  • Dans le répertoire components, créer un composant ItemsList.vue.
  • Ce composant doit faire la même chose que VirusesView.vue (qui n'est plus nécessaire) des TPs précédents, mais en utilisant la version modifié de CheckedList.vue pour :
    • que chaque item fasse apparaître le nom du virus, son prix, les promotions, le champ numérique de quantité et le bouton pour mettre au panier.
    • quand l'événement clic sur le bouton d'item est reçu, ajouter cet item ainsi que sa quantité dans le panier de l'utilisateur courant stocké dans le store (NB : dans shopUser). Attention, si cet item est déjà présent dans le panier, il faut juste augmenter sa quantité et non pas ajouter une nouvelle entrée dans le panier.
    • quand l'événement clic sur le bouton de fin de liste est reçu, ajouter tous les items et leur quantité dans le panier de l'utilisateur.

 

5.3°/ Le stockage du panier en mémoire

  • Modifier le store shop.js, afin de pouvoir gérer un panier pour l'utilisateur courant
  • Il existe plusieurs solutions pour faire ces modifications, la plus "simple" étant d'ajouter une variable dans le state représentant le panier, par exemple nommée basket.
  • De plus, on peut prendre en compte le côté back-end/BdD, afin de gérer des objets avec la même structuration côté front-end.
  • Pour ce TP, c'est la solution qui est choisie, sachant que côté BdD, un panier a la structure suivante (en syntaxe mongoose) :
 
{
  items: [{  
    item: {type: Schema.Types.ObjectId, required: true, ref: 'Item'},
    amount: {type: Number, required: true, min:0}, 
  }]
}
  • D'après cette structuration, un panier est un objet contenant un champ items, du type tableau. Chaque élément de celui-ci est un objet avec un champ item, qui est l'id d'un item existant, et un champ amount qui est le nombre de ce type d'item mis dans le panier.

 

  • Le problème de cette solution est que le store ne gère qu'un seul utilisateur courant et un seul panier.
  • Si on change d'utilisateur courant, le panier devient invalide et on doit le remettre à vide. Par conséquent, si on revient à l'utilisateur précédent, son panier est perdu.
  • La solution consiste à reporter toute modification du panier de l'utilisateur courant en BdD et dès qu'on change d'utilisateur, de récupérer son panier en BdD, le tout via des requêtes à l'API.
  • Mais comme ce TP fonctionne avec une source locale de données, il faut pouvoir simuler ces requêtes et "enregistrer" le panier dans celle-ci.
  • Pour ce faire, il suffit de constater que le contenu de data.js est chargé en mémoire. On peut donc tout à fait modifier les objets représentant les utilisateurs, stockés dans le tableau shopuser. Si on connait l'id d'un utilisateur, on peut retrouver son objet dans le tableau, puis récupérer ou lui ajouter/modifier un champ basket représentant son panier. 
  • Si on recharge l'application, ces ajouts seront perdus, mais il restent en mémoire même si on change d'utilisateur courant dans le store.

 

  • La solution à suivre est donc d'ajouter
    • dans localsource.service.js :
      • une méthode (par ex, updateBasket() ) pour ajouter/modifier le champ basket d'un utilisateur stocké dans shopuser, en fournissant un objet avec la structuration donnée ci-dessus.
      • une méthode (par ex, getBasket() ) pour récupérer la valeur du champ basket d'un utilisateur stocké dans shopuser.
      • ATTENTION : comme vu dans le TP 1, puisque le panier va être modifié par l'application, il ne faut surtout pas renvoyer directement le panier trouvé dans la source locale mais un clone. Pour ce faire, le plus simple est d'utiliser les fonction parse() et stringify(). Par exemple :
function getBasket(data) {
  ...
  let user = ... // cherche un utilisateur dans usershop
  if (user.basket == null) user/basket = [] // si pas encore de panier (c.a.d. null ou undefined), le créer
  let basket = JSON.parse(JSON.stringify(user.basket)) // clone son panier
  return {error: 0, status: 200, data: basket}
}
  •  
    • dans shop.service.js : des méthodes qui appellent celles de localsource.service.js
    • dans le store:
      • une variable (par ex. nommé basket) pour représenter le panier,
      • des actions permettant mettre à jour cette variable, via les fonctions de service.
      • compte tenu des opérations possibles sur le panier, il faut au moins 4 actions pour :
        • récupérer le panier existant de l'utilisateur courant via shop.service afin d'initialiser le contenu de basket,
        • ajouter à basket un item avec une certaine quantité (ou augmenter la quantité si l'item est déjà dedans) et mettre à jour en Bdd/source locale via shop.service
        • supprimer totalement un item et mettre à jour en Bdd/source locale via shop.service
        • vider totalement le panier et mettre à jour en Bdd/source locale via shop.service

 

5.4°/ Le composant panier

  • Dans le répertoire components, créer un composant BasketList.vue.
  • Ce composant doit utiliser la version modifiée de CheckedList.vue pour :
    • afficher chaque item du panier avec un bouton supprimer après,
    • afficher un bouton "vider le panier" après cette liste,
    • quand l'événement clic sur le bouton d'item est reçu, supprimer cet item du panier de l'utilisateur.
    • quand l'événement clic sur le bouton après la liste est reçu, supprimer le contenu du panier.
  • Lors de son chargement, BasketList doit demander au store de récupérer le panier de l'utilisateur courant.
  • Cela permet ensuite d'afficher les items du panier de l'utilisateur courant, stocké dans le store (cf. 4.3)
  • De plus, le composant doit afficher un bouton "Acheter" permettant de valider le panier et créer une commande. Quand on clique sur ce bouton, il faut normalement envoyer une requête à l'API avec l'id utilisateur et le panier actuel comme données, pour recevoir en retour un objet représentant la commande. Dans la BdD, cette commande est représentée par le schéma OrderSchema suivant  :
{
  items: [{    
    item: {
      name: {type: String, required: true},
      description: {type: String},
      price: {type: Number, required: true, min: 0}, 
      promotion: [{
        discount: {type: Number, required: true, min: 0}, 
        amount: {type: Number, required: true, min: 0}, 
      }],
      object: {type: String, required: true}
    },
    amount: {type: Number, required: true, min:0}, 
  }],
  date: {type: Date, default: Date.now},
  total: {type: Number, required: true, min:0}, 
  status: {type: String, require: true, enum: ['waiting_payment', 'finalized', 'cancelled']},
  uuid: {type: String, required: true }, // value MUST BE an unique uuid v4
}
  • On remarque que cette structure est relativement similaire à celle du panier, excepté que l'on a un tableau avec des items complets (et pas seulement leur _id), plus 4 champs supplémentaires (date, total, statut, uuid)
  • Dans ce TP, on considère que c'est normalement à l'API de créer un tel objet à partir d'un panier, puis de renvoyer au front la valeur du champ uuid.
  • Cependant, cela laisse deux solutions :
    • soit on transmet le panier courant stocké dans le store du front,
    • soit l'API utilise directement le panier courant de l'utilisateur.
  • Normalement, les deux solutions sont équivalentes puisque toute modification du panier dans le front est reportée dans le back.
  • Cependant, il est possible que la mise à jour dans le back soit légèrement retardée (délai réseau, back indisponible temporairement, ...) et que pendant ce temps, l'utilisateur demande la création de la commande.
  • Pour éviter de créer cette commande avec un panier par encore mis à jour, il faut prendre la première solution. 

 

  • Comme pour le panier, il faut simuler les requêtes à l'API pour créer cette nouvelle commande en modifiant les données de la source locale. Il faut donc ajouter des méthodes dans 2 fichiers :
    • dans localsource.service.js :
      • une méthode (par ex, orderBasket() ) qui prend en paramètre un id utilisateur et un objet panier, et qui le champ orders de l'utilisateur (stocké dans shopuser),en lui ajoutant un objet avec la structuration donnée ci-dessus.
      • cette méthode doit  d'abord parcourir les items du panier pour retrouver les objets virus correspondant et le ajouter au tableau items de la commande. Ensuite, elle doit ajouter les champs total (= coût total de la commande promotions comprises), l'uuid, (tiré aléatoirement), le statut égal à "waiting_payment" et la date avec la date courante. Ensuite, elle ajoute cet objet au tableau orders. Enfin, elle renvoie une réponse dont le champ data est un objet {uuid: ...}.
    • dans shop.service.js :
      • une méthode qui appelle la fonction de localsource.service.js et renvoie la valeur retournée (= un uuid si la commande est correcte).

 

  • Contrairement à la gestion du panier, il n'y a pas besoin de stocker la commande dans le store. C'est pourquoi, il n'y a pas besoin d'ajouter des actions dans shop.js.
  • Le composant BasketList doit donc directement importer shop.service afin d'accéder à la méthode pour créer une nouvelle commande. 
  • Quand on clique sur le bouton "Acheter", il faut donc :
    • appeler la fonction de service avec l'id utilisateur+panier courant en paramètres, et récupérer la valeur retournée
    • si la valeur retournée est un uuid, vider le panier puis suivre la route /shop/pay/ avec en paramètre l'uuid.

 

  • A noter que le tableau shopuser fourni dans data.js contient déjà un utilisateur drmad avec un champ orders contenant une commande.

 

6°/ Payer une commande

  • Dans le répertoire views, créer un composant ShopPay.vue.
  • Ce composant a une props nommée orderUuid, représentant l'uuid d'une commande (NB : et pas son champ _id) et affiche :
    • un champ texte pour saisir l'uuid d'une commande. Si la props mentionnée ci-dessus est définie, alors ce champ de saisie est initialisé avec la valeur de la props.
    • un bouton "Payer". Quand on clique sur celui-ci, et si l'uuid fourni correspond bien à une commande existante de l'utilisateur courant, cette commande passe à l'état "finalized". Dans ce cas, on suit la route permettant d'afficher les commandes de l'utilisateur courant.

NB : dans le TP suivant, la gestion d'un compte bancaire sera ajoutée, ce qui permettra de vérifier si le montant de la commande correspond effectivement à une transaction bancaire de paiement.

 

  • Comme avec les autres opérations, il faut mimer les appels à l'API en modifiant la source locale. Il faut donc créer les fonctions dans localsource.service.js :
    • une méthode (par ex, getOrder() ) prenant en paramètre l'uuid d'une commande et l'id d'un utilisateur et qui renvoie les informations de la commande (c.a.d l'objet stocké dans orders). (NB : cette méthode doit renvoyer un clone de la commande trouvée puisque celle-ci peut être modifiée)
    • une méthode (par ex, payOrder() ) prenant en paramètre l'uuid d'une commande et l'id d'un utilisateur et qui représente le fait de payer en modifiant le statut d'une commande à "finalized"
  • Il faut également créer les fonctions de service associées, pour pouvoir les appeler depuis ShopPay.
  • Comme avec le panier et la création d'une commande, on remarque qu'il n'y a pas besoin de stocker la commande dans le store, donc aucune raison de créer une action.

  

7°/ Commandes passées

  • Dans le répertoire views, créer un composant ShopOrders.vue.
  • Ce composant affiche la liste des commandes de l'utilisateur courant, que l'on récupère auprès de l'API/source locale (cf. remarques ci-dessous)
  • Chaque élément de la liste affiche son montant, son état (c.a.d. champ status), et est suivi par des boutons "Payer" et "Annuler" si la commande est dans l'état "waiting_payment".
  • Quand on clique sur "Payer", on suit la route /shop/pay/:orderUuid en mettant comme valeur d'orderUuid l'iuuid de la commande que l'on veut payer.
  • Quand on clique sur "Annuler", la commande doit passer à l'état "cancelled".

 

  • Là encore, il faut mimer les appels l'API en écrivant les fonctions de service et en modifiant localsource.service.js. Il faut donc ajouter des méthodes qui permettent de récupérer toutes les commandes d'un utilisateur et changer le statut d'une commande avec la valeur "cancelled".
  • NB 1 : si un utilisateur n'a pas encore fait de commande, renvoyer un tableau vide.
  • NB 2 : si des commandes existent, il faut là encore renvoyer leur clone, puisqu'elles peuvent être modifiées par l'application.
  • Comme pour le panier et le paiement, il n'est normalement pas nécessaire de stocker les commandes dans le store puisqu'il n'y a que ce composant qui les utilise.
  • Cependant, comme elles sont liées à l'utilisateur courant et que ce dernier EST dans le store, il est pertinent de stocker les commandes dans cet utilisateur courant, comme c'est fait en BdD/source locale.
  • Cela implique qu'il faut ajouter dans le store :
    • une action qui utilise une fonction de service pour récupérer les commandes de l'utilisateur courant. NB: en vue V3, même si on ajoute un nouveau champ à l'utilisateur courant (par ex, orders pour stocker ce qui a été reçu), vue va mettre en place son observation. Ce n'est pas le cas en V2.
    • une action qui utilise une fonction de service pour mettre à jour le statut d'une commande donnée à "cancelled"