Dans ce tutoriel, vous allez apprendre à gérer un panier: ajout/retrait d'articles du panier, affichage du panier, transformation du panier en commande.
Contexte du tutoriel:
Les exemples ci-dessous sont un prolongement des tutoriels "Liste dynamique" et "Session utilisateur" ; si vous ne les avez pas encore lus, nous vous invitons à en prendre connaissance maintenant.
Dans les précédents tutos, vous avez appris à générer une liste d'articles à la volée (l'utilisateur voit la liste, à jour en temps réel, des articles dans la base de données) et à identifier l'utilisateur qui se connecte.
Nous souhaitons maintenant qu'un utilisateur, une fois connecté, puisse ajouter des articles dans son panier et commander le contenu de ce panier.
Prérequis
Pour commencer, lire les tutos: "Liste dynamique" et "Session utilisateur".
Pour faire fonctionner l'exemple de ce tutoriel, vous devez disposer d'un serveur web, du langage php et d'une base de données MySQL. Des compétences minimales en php et sql sont nécessaires.
Moyennant quelques adaptations, vous pouvez tester cet exemple avec une autre base et/ou un autre langage: la seule contrainte est de produire des documents XML conformes aux spécifications AppMobile.
Si vous n'avez pas de serveur à disposition, des offres d'essai gratuites existent chez de nombreux hébergeurs - une simple recherche du type "hébergement gratuit mysql php" sur votre moteur de recherche préféré suffira à vous apporter une solution.
Fonctionnalités recherchées
Nous disposons déjà des scripts permettant d'afficher le catalogue produits: liste des catégories d'articles, liste des articles d'une catégorie et détail des données d'un article (voir tuto "Liste dynamique").
Nous avons aussi tout ce qu'il faut pour gérer la connexion et la déconnexion d'un utilisateur (voir tuto "Session utilisateur").
Il nous reste à réaliser:
- un script pour ajouter/supprimer un article du panier
- un script pour afficher le panier
- un script pour transformer le panier en commande
- un script pour afficher la liste des commandes et un autre pour afficher le détail d'une commande
Nous apporterons quelques amélioraions aux scripts des précédents tutos:
. dans la fiche article, affichage de la quantité dans le panier et ajout de boutons [+] [-] pour appeler le script d'ajout/retrait du panier
. dans la liste des articles, affichage pour chaque article de la quantité en panier.
Structure du panier
Un utilisateur peut avoir plusieurs articles dans son panier, et un article peut figurer dans les paniers de plusieurs utilisateurs. D'un point de vue base de données, le panier est une relation "many to many" entre la table des utilisateurs et celle des articles.
Nous allons donc créer une table de liaison amdoc_orders).
- Chaque ligne de cette table renvoie d'une part vers un utilisateur (champ id_user), et d'autre part vers un article (champ id_prod).
- La quantité de cet article que l'utilisateur a mise dans le panier est stockée dans le champ qty.
- Une information sur le statut de cette ligne est nécessaire: cette ligne fait-elle partie d'un panier en cours? d'un panier validé (c'est à dire une commande)? d'une commande traitée? expédiée? réceptionnée par le client? Cette information sera stockée dans le champ status.
Nota: cette information est traitée ligne par ligne et non commande par commande, car toutes les lignes ne suivront pas nécessairement le même sort (exemple: tous les articles sont livrés, sauf un qui est en rupture de stock...) - Une fois le panier validé, il devient une commande, à laquelle il faut attribuer un numéro qui sera utile en pratique pour son traitement (suivi administratif et logistique...): c'est le champ id_order (dont la valeur sera à zéro tant que le panier n'est pas validé).
- Enfin, cette table pourra aussi contenir des informations complémentaires relatives à la ligne de commande; à titre d'exemple, prévoyons un champ commentaire (comment) qui pourra être saisi par l'utilisateur au moment d'ajouter un article au panier.
On peut aussi imaginer de tracer la date/heure de traitement, d'expédition, la référence du transporteur, du colis...
Table panier/commandes :
CREATE TABLE IF NOT EXISTS `amdoc_orders` ( `id_user` int(11) NOT NULL, `id_prod` int(11) NOT NULL, `qty` int(11) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `id_order` int(11) NOT NULL, `comment` varchar(300) DEFAULT '', PRIMARY KEY (`id_user`,`id_prod`,`id_order`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Ajouter/supprimer un article au panier
Le script doit:
- récupérer l'id de l'utilisateur (id_mvsappmobile, cf tutoriel "Session utilisateur").
- récupérer l'id de l'article et la quantité à ajouter (Q1):
positive=ajout, négative=retrait - interroger la base pour vérifier si l'utilisateur est connecté, si le produit existe, et s'il existe une ligne non validée pour cet article et cet utilisateur (une ligne déjà validée est une commande passée, elle n'est plus modifiable).
- si produit inexistant ou utilisateur non connecté, on sort.
- si ligne inexistante dans le panier, on ajoute une nouvelle ligne.
- si ligne existante, on stocke dans le panier (champ qty) la quantité antérieure (Q2) + la nouvelle quantité (Q1).
Nota: en cas de retrait (Q1 négative), si la nouvelle quantité (Q2+Q1) est négative ou nulle, on supprime la ligne du panier.
Ce script relève de la pure gestion de base de données côté serveur, il n'implique aucune action côté smartphone. Simplement, la page ou la liste qui appelle ce script devra traiter sa valeur de retour: quantité en panier si tout se passe bien, message d'erreur sinon.
Attention: nous avons décidé d'utiliser les anciennes fonctions mysql_* dans les scripts de démo, pour une compatibilité plus large. Vous ne devriez PAS les utiliser en production, mais télécharger les scripts des modules téléchargeables à la place (basés sur la librairie PDO, plus complets et plus sûrs).
cart_add.php :
<?php include('config.php'); $id=$_GET["id"]; $id_mvs=$_GET["id_mvsappmobile"]; $toAdd=$_GET["qty"]; function quit_script($errorMsg) {//to be customized depending on your app's specific return neeeds in matter of return value die($errorMsg); } if(!is_numeric($toAdd)||$toAdd==0) { quit_script("param incorrect"); } $db = @mysql_connect($dbhost, $dbuser, $dbpassword); if(!$db) { quit_script("Pas de connexion"); } // MySQL connection problem if(!@mysql_select_db($dbname)) { quit_script("Pas de connexion"); } // MySQL DB selection problem $query="SELECT u.id_user, p.id_prod, o.qty FROM `amdoc_users` u left join `amdoc_orders` o on u.id_user=o.id_user and o.id_prod='$id' and o.status=0 left join `amdoc_products` p on p.id_prod='$id' and p.del=0 where u.id_mvsappmobile='$id_mvs'"; /* user not connected:empty result -- unexisting prod:id_prod=null -- prod not in cart yet:qty=null */ $result = @mysql_query($query,$db); if(!$result) { quit_script("ERREUR MySQL"); } if(mysql_num_rows($result)==0) { quit_script("Unlogged"); } extract(mysql_fetch_assoc($result)); if($id_prod===null) { //cet art. n'existe pas dans la table produits quit_script("Produit inexistant"); } if($qty===null) if($toAdd>0) { //l'art. n'existe pas dans le panier->on ajoute si positif, on ignore si négatif $req="INSERT INTO `$db_tbl_ord` (`id_user`,`id_prod`,`qty`, `id_order`, `status`) values ('$id_user','$id_prod',$toAdd,'0','0')"; $res=$toAdd;} else {$req="";$res=0;} else if($toAdd<0 && $qty<=(-$toAdd)) { //panier inférieur ou égal à la quantité à retirer -> on supprime la ligne $req="DELETE FROM `$db_tbl_ord` where `id_user`='$id_user' and `id_prod`='$id_prod' and `status`=0"; $res=0;} else { //ds tous les autres cas $req="update `$db_tbl_ord` set `qty`=`qty`+$toAdd where `id_prod`='$id' and `id_user`='$id_user' and `status`=0"; $res=$qty+$toAdd;} if($req=="") { quit_script("No action to do"); } $result = @mysql_query($req,$db); @mysql_close($db); if(!$result) quit_script("Error writing to base.<br />"); echo($res); ?>
Ajout d'un bloc "panier" à la fiche article
Nous avons maintenant un petit script qui nous permet d'ajouter des articles au panier ou d'en retirer... C'est magnifique, mais comment l'appelle-t-on?
Nous vous proposons d'ajouter un simple petit bloc sur la fiche article.
Pour éviter que l'appui sur le bouton + ou - recharge la page, et produise un effet de clignotement peu agréable (surtout avec un débit parfois lent sur un téléphone), nous vous proposons d'ajouter une petite touche de javascript pour appeler le script en AJAX et ne raffraîchir que l'affichage de la quantité en panier.
art_description.php :
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script> //<![CDATA[ function affiche(elementID,texttoprint){ document.getElementById(elementID).innerHTML = texttoprint; //todo: here just add a little test on "texttoprint" in order to display an error message if necessary if(texttoprint=="0") { document.getElementById('commentForm').style.display = "none"; document.getElementById('comment').innerHTML = ""; } else document.getElementById('commentForm').style.display = "block"; } function ajaxPost(myurl,param){ if (window.XMLHttpRequest){xmlhttp=new XMLHttpRequest();}// code for IE7+, Firefox, Chrome, Opera, Safari else {xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");}// code for IE6, IE5 xmlhttp.open("post",myurl,false); xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); xmlhttp.send(param); affiche("qty",xmlhttp.responseText); } function ajaxGet(myurl,param){ if (window.XMLHttpRequest){xmlhttp=new XMLHttpRequest();}// code for IE7+, Firefox, Chrome, Opera, Safari else {xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");}// code for IE6, IE5 xmlhttp.open("get",myurl+"?"+param,false); xmlhttp.send(null); affiche("qty",xmlhttp.responseText); } //]]> </script> </head> <?php require_once('config.php'); $id=$_GET["id"];//produit $uid=$_GET["id_mvsappmobile"];//user $db = mysql_connect($dbhost, $dbuser, $dbpassword); mysql_select_db($dbname); $query = "SELECT p.name, p.descr,p.pic, p.price, c.qty, c.comment FROM $db_tbl_prod as p left JOIN $db_tbl_ord as c ON p.id_prod = c.id_prod and c.id_user=(select id_user from $db_tbl_user where id_mvsappmobile='$uid') and c.id_order=0 where p.id_prod='$id'"; $result = mysql_query($query,$db); mysql_close($db); $l = mysql_fetch_assoc($result); extract($l);//$name=$l["name"]; $descr=$l["descr"]; $qty=$l["qty"]; $comment=$l["comment"]; if($qty==null) $qty=0; if($comment==null) $comment=""; ?> <body style="width:100%;margin:0;padding:0;"> <div><h1 style="text-align:center;margin-top:5px;margin-bottom:5px"><b><?echo($name);?></b><br /></h1></div> <div><?php echo("<img src='".$urlprefix."/pic/$pic' />");?></div> <div style="width:80%;margin-right:auto;margin-left:auto;"> <p style="text-align:right;margin-top:0px;"><?echo(number_format($price, 2, ',', ' ')." €");?><br /></p> <p><?echo($descr);?><br /></p> <p> <? echo("<a style='text-decoration: none' href=\"javascript:ajaxGet('".$urlprefix."/cart_add.php','id=$id&id_mvsappmobile=$uid&qty=-1')\">");?> <img src="<?php echo($urlprefix);?>pic/minus.png" style="width:24px;height:24px;vertical-align:-7px;" /> </a> <span id='qty' style='border:1px solid;padding:0 15px'><?echo($qty);?></span> <? echo("<a style='text-decoration: none' href=\"javascript:ajaxGet('".$urlprefix."/cart_add.php','id=$id&id_mvsappmobile=$uid&qty=1')\">");?> <img src="<?php echo($urlprefix);?>pic/plus.png" style="width:24px;height:24px;vertical-align:-7px;" /> </a> </p> </div> </body>
Nota: Le script appelé, cart_add.php, attend que les paramètres lui soient passés en "GET". La fonction javascript AjaxPost() n'est pas utilisée, mais figure ici pour que vous puissiez facilement adapter les scripts en fonction de vos besoins.
Afficher le panier
Nul besoin ici de longues explications, tout a été expliqué plus haut quant à la structure du panier.
L'affichage se résume à une requête pour récupérer les articles et leurs quantités dans le panier de l'utilisateur connecté, une boucle pour ajouter un item dans la liste XML pour chaque article trouvé, et quelques cas particuliers à traiter (utilisateur non connecté, panier vide...).
cart_list.php :
<?php include('config.php');//config data, especially $dbhost,$dbuser,... for DB connection $id_mvs=$_GET['id_mvsappmobile']; //session ID $xml="<?xml version='1.0' encoding='UTF-8'?>\r\n<items version='1.0' type='1' reload='1' locate='0'>";//XML list description header ("Content-Type:text/xml; charset=utf-8");//http header to tell it's XML content function add_item(&$xmlstring, $typ, $img, $tx1, $tx2, $tx3, $link) { $xmlstring.=" <item type='$typ'> <img>$urlprefix/$img</img> <txt1>$tx1</txt1> <txt2>$tx2</txt2> <txt3>$tx3</txt3> <link type='1' url='$link' title='Panier' /> </item>"; } function quit_script(&$xmlstring, $pic, $Msg1, $Msg2="", $Msg3="",$lnk="") { add_item($xmlstring, 1, $pic, $Msg1, $Msg2, $Msg3, $lnk); echo($xmlstring."\r\n</items>"); exit; } $db = @mysql_connect($dbhost, $dbuser, $dbpassword); if(!$db) { quit_script($xml,"pic/noConnec.png","Pas de connexion,","Merci de réessayer plus tard."); } //MySQL connection problem if(!@mysql_select_db($dbname)) { quit_script($xml,"pic/noConnec.png","Pas de connexion,","Merci de réessayer plus tard."); } //MySQL DB selection problem $query="SELECT u.id_user, p.id_prod, p.name, p.price, p.del, p.pic, o.qty, c.name as catname FROM `amdoc_users` u left join `amdoc_orders` o on u.id_user=o.id_user and o.status=0 left join `amdoc_products` p on p.id_prod=o.id_prod left join `amdoc_categories` c on c.id=p.cat where u.id_mvsappmobile='$id_mvs'"; //empty if not connected -- one single line with id_prod==null if empty cart -- one or more lines if cart not empty $result = @mysql_query($query,$db); if (!$result) { quit_script($xml,"pic/noConnec.png","Pas de connexion","Merci de réessayer plus tard."); } //Problème MySQL if(mysql_num_rows($result)==0) { quit_script($xml,"pic/noSession.png","Vous n'êtes pas connecté,","Merci de vous identifier.","(onglet \"Accueil\")"); }//no connected user for this session while ($l = mysql_fetch_assoc($result)) { extract($l); if($del!=0)$name="[Supprimé: $name]";//attirer l'attention de l'utilisateur si un produit du panier n'existe plus... //echo($query." ***** ".print_r($l,true)); if($id_prod===null) { quit_script($xml,"pic/emptyCart.png","Panier vide","Consultez la carte pour faire votre choix"); } add_item($xml,'1',"pic/th_$pic",$catname,"$name".($qty>1?" (x$qty)":""),number_format($price, 2, ',',' ')." €","$urlprefix/art_description.php?id=$id_prod&id_mvsappmobile=$id_mvs"); //for a picture whose name 'picname.png' you store in product DB entry, create a thumbnail picture named 'th_picname.png' } echo("$xml </items>"); ?>
Transformer le panier en commande
Cela revient à
- passer pour chaque ligne du panier de l'utilisateur, le statut (champ status de la valeur zéro (panier en cours) à la valeur 1 (panier validé).
- attribuer un numéro de commande à ces lignes
- avertir la personne compétente afin d'assurer le traitement de la commande. Ici nous envoyons un e-mail à l'adresse définie dans config.php. A vous d'adapter la fonction notify_order() pour définir l'action voulue: communiquer l'information à votre gestion commerciale ou votre site de vente en ligne, par exemple.
cart_order.php
<?php include('config.php'); $id_mvs=$_GET["id_mvsappmobile"]; function quit_script($errorMsg) {//to be customized depending on your app's specific neeeds in matter of return value die("<p>$errorMsg</p></body></html>"); } function notify_order($msg) {//to be customized depending on your app's specific neeeds in matter of return value $headers = "From: \"SpeedZa\"<speedza@fpierrat.fr>\n"; $headers .= "Reply-To: speedza@fpierrat.fr\n"; $headers .= "Content-Type: text/plain; charset=\"utf-8\"\n"; $headers .= "Content-Transfer-Encoding: 8bit"; $msg = wordwrap($msg, 70, "\r\n");//lines over 70 characters -> split with wordwrap() $mailRet=mail($emailaddress, "Commande smartphone", $msg, $headers);//$emailaddress defined in config.php } echo("<html><head><meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /></head><body>"); $db = @mysql_connect($dbhost, $dbuser, $dbpassword); if(!$db) { quit_script("noConnection"); } // MySQL connection problem if(!@mysql_select_db($dbname)) { quit_script("noConnection"); } // MySQL DB selection problem $query = "select p.name as name, cat.name as catname, o.qty, o.comment, u.id_user from `$db_tbl_ord` o, `$db_tbl_prod` p, `$db_tbl_cat` cat, `$db_tbl_user` u where o.`id_user`=u.id_user and u.id_mvsappmobile='$id_mvs' and o.`status`=0 and p.`id_prod`=o.`id_prod` and p.cat=cat.id and p.del=0"; $result = @mysql_query($query,$db); if(!$result) quit_script("queryFail"); // MySQL query problem if (mysql_num_rows($result) == 0) quit_script("emptyCart_or_notConnected"); while ($l = mysql_fetch_assoc($result)) { extract($l);//$name=,$catname,$qty,$comment, $id_user $message .= "\r\n$qty x $catname -- $name\r\n"; if($comment!=null) $message.=" -> $comment\r\n"; } $query = "delete from o using $db_tbl_ord as o, $db_tbl_prod as p where o.`status`=0 and o.`id_user`=$id_user and p.id_prod=o.id_prod and p.del!=0";//delete lines concerning eventually deleted items. Todo: user should be explicitly notified. $result2 = @mysql_query($query,$db); if(!result2) echo("Un problème a été rencontré avec des produits supprimés.");//debug msg, unnecessary for user to know $query = "UPDATE $db_tbl_ord set id_order = ((SELECT selected_value FROM (SELECT MAX(id_order) AS selected_value FROM $db_tbl_ord) AS sub_selected_value) + 1), `status`=1 WHERE `id_user`=$id_user and `status`=0";//définit un nouvel ID de commande (max+1) et l'assigne à chaque ligne du panier $result3 = @mysql_query($query,$db); if(!result3) quit_script("Erreur: votre commande n'a pas pu être validée."); notify_order($message); echo("<p style='margin-top:20px'>Votre commande a été envoyée à $emailaddress</p><p style='margin-top:10px;'>Sa préparation sera finalisée dès votre arrivée.</p><p>Le paiement aura lieu au comptoir.</p> <p style='margin:10px 0;'>A bientôt!</p> <p style='border-top:solid 1px;border-bottom:solid 1px;margin-top:40px;text-align:center;'>Copie de la commande envoyée :</p> <p>".str_replace("\r\n","<br />",$message)."</p></body></html>"); ?>
Le script une fois réalisé, il reste à prévoir un endroit d'où l'utilisateur pourra l'appeler. Ajoutons un item "Valider la commande" à la liste des articles en panier:
Ajout à: cart_list.php
<?php add_item($xml, "1", "pic/valid-ico.jpg", "", "Valider la commande", "", "$urlprefix/cart_order.php?id_mvsappmobile=$id_mvs"); ?>
Et après...
Il sera peut-être opportun de prévoir pour le back-office une interface de suivi des commandes (à traiter, en cours, expédiées...) et de gestion du catalogue produits. Ce flux n'est pas traité dans le présent tutoriel:
- D'abord, parce que ces tâches sont généralement traitées sur internet, dans une interface web classique, et non sur smartphone.
- Ensuite, parce que l'appli smartphone de prise de commande vient souvent en complémeent d'outils de gestion existants (gestion commerciale, facturation, vente en ligne...). Il sera souvent plus judicieux de mettre à profit toute la souplesse du système AppMobile pour créer une interface client qui se connecte aux données existantes.
Si toutefois vous souhaitiez disposer d'une simple interface de visualisation et de traitement des commandes, l'organisation des données présentées dans ce tutoriel vous offre une base de départ pour le faire simplement.
TODO: Pour la gestion des produits, adapter et mettre en ligne les scripts "admin" existants.