*/ ?>
Gestion d'un panier

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:

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).

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:

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 [ - ][Qty][ + ] 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 à

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:

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.