Horaires de ma femme
Ceci est une application qui résoud un problème concret.
Ma femme est aide-soignante dans un EMS. Ses heures de travail changent d'un jour à l'autre. Elle apprend au courant du mois ses horaires pour le mois suivant. Jusqu'ici elle arrivait à se procurer une photocopie des horaires. Malheureusement, un règlement absurde interdit de les photocopier et la direction a récemment décidé d'appliquer ce règlement. Nous ferions mieux d'appeler le syndicat, mais comme je suis informaticien, j'écris plutôt une application.
L'application doit permettre à ma femme de saisir rapidement son horaire du mois suivant quand il est affiché. Comme j'ai moi aussi besoin de connaitre les horaires de ma femme, et que ma mère aimerait les savoir aussi. L'application doit pouvoir publier l'horaire sur internet.
Cette application fonctionne, mais elle est loin d'être finie.
application.js
<source lang="javascript" enclose="none"> Ext.setup({
onReady: function() {
Ext.regModel('Day', {fields: ['date','horaire']} ); var list = new Ext.List({ itemTpl : '{date}: {horaire}',
singleSelect: true,
//pour raccourcir le code, je crée le store ici. store: new Ext.data.JsonStore({ model : 'Day', // voir la définition de loadData plus bas data: loadData()}) });
// Les configurations qui vont être utilisées pour les boutons. // Les attributs qui sont pareils pour tous les boutons (xtype, height, width, listeners) // vont être ajoutés par défaut par la fonction makeButtonPannel var buttons1 = [{text: '1'},{text: '2'},{text: '3'},{text: '3s'},{text: '4'},{ text: '5'}]; var buttons2 = [{text: 'ce'},{text: 'f'},{text: 'r'},{text: 'v'},{text: 'fo'},
{
// Les listeners sont des fonctions qui sont appelées lorqu'un événement a lieu. // Ici on a un listener qui réagit à l'événement 'tap', qui est envoyé // quand on touche le bouton. Pour un exemple d'utilisation d'autres événements // voir sencha-touch-1.1.0/examples/kitchensink/src/demos/touch.js // Comme ce bouton a déjà un attribut listeners, il ne va pas recevoir // les listeners par défaut donnés par makeButtonPanel listeners: { 'tap': function () {selectPreviousNode(list);} }, // Ce dernier bouton contient une icone à la place du texte. // Ca ne fonctionne pas très bien avec l'agrandissement des boutons. // Pour faire cela correctement, voir // http://www.sencha.com/forum/showthread.php?150028-Scaling-button-icons iconCls: 'arrow_up', iconMask: true // necessary if the icon is encoded as base64
}];
var size = screen.width/Math.max(buttons1.length,buttons2.length); var buttonPanel1 = makeButtonPannel(buttons1,size,list); var buttonPanel2 = makeButtonPannel(buttons2,size,list);
var titleToolbar = new Ext.Toolbar({dock: 'top',title: 'horaires' }); var bottomToolbar = new Ext.Toolbar({
dock: 'bottom', defaults: {iconMask: true},
layout: {pack: 'center'}, items: [{iconCls: 'delete', text: 'reset', listeners: {'tap': function () {resetData(list);}} },
{iconCls: 'action', text: 'save',
listeners: {'tap': function () {saveData(list);}}
},
{iconCls: 'compose',
text: 'post',
listeners: {'tap': function () {postData(list);}}
}]
});
new Ext.Panel({ fullscreen: true,
// Le layout 'fit' remplit tout avec le seul item qu'il contient // Sans cette ligne la liste dépasserait sous les boutons // voir la doc http://docs.sencha.com/touch/1-1/#!/api/Ext.layout.FitLayout
layout: 'fit', items: [list], dockedItems: [titleToolbar,bottomToolbar,buttonPanel2,buttonPanel1] });
list.select(0);
}});
// Crée un panel de boutons à partir de: // buttons: un array de configurations de boutons // size: le coté d'un bouton (ils sont carrés) // list: la liste sur laquelle agissent les boutons function makeButtonPannel(buttons, size, list){
return new Ext.Panel({ // quand on utilise ce panel comme dockedItem d'un autre panel // il apparait en bas dock: 'bottom', layout: { // layout qui pose ses items côte à côte horizontalement // voir http://docs.sencha.com/touch/1-1/#!/api/Ext.layout.HBoxLayout type : 'hbox', pack : 'center' }, // hauteur du panel: size passé en argument de la fonction height: size, // attributs qui vont s'ajouter aux attributs des items. defaults: { // Les items auront le type Button // pour la correspondance entre xtypes et classes // voir http://docs.sencha.com/touch/1-1/#!/api/Ext.Component xtype: 'button', height: size, width: size, listeners: { 'tap': function(button, event){ setHoraire(button.text,list);} } }, // la liste de boutons passés en argument items: buttons });
}
// Positionne l'horaire à la date sélectionnée dans la liste // et sélectionne l'élément suivant. function setHoraire(horaire,list){
// Il doit y en avoir un unique élément sélectionné var selectedRecord = list.getSelectedRecords()[0]; var index = list.indexOf(selectedRecord); // C'est un modèle. Pour le modifier il faut utiliser la fonction set // http://docs.sencha.com/touch/1-1/#!/api/Ext.data.Model selectedRecord.set('horaire',horaire); // Il faut rafraichir le node pour que le changement soit visible list.refreshNode(index); // sélectionner la ligne suivante if(index < list.getStore().getCount() - 1){ list.select(index + 1); }
}
// Recule la sélection d'une position dans la list function selectPreviousNode(list){
var selectedNode = list.getSelectedNodes()[0]; //il doit y en avoir un var selectedIndex = list.indexOf(selectedNode); var newIndex = selectedIndex === 0 ? 0 : selectedIndex - 1; list.select(newIndex);
}
// Enregistre les horaires sous la forme d'un string, // par exemple "3s|4|r|..."
function saveData(list){
// Array de tous les modèles Day var records = list.getStore().data.getRange(); // string "3s|4|r|..."
var horaires = DAY.recordsToHoraires(records);
// localStorage est un objet fourni par javascript (html 5). // Les données qu'on y met sont enregistrées dans le browser // et sont disponibles à chaque fois qu'on va sur la page. localStorage["horaires"] = horaires;
}
// Lit les données enregistrées et les retourne sous forme d'un array // de configuraions du modèle Day // par exemple [{date: "Thu 1.12.2011", horaire: "4"}, {...}, ... ] function loadData(){
// Ce programme n'est pas encore capable de gérer plusieurs mois. // On décide arbitrairement qu'on est en décembre 2011 var year = 2011; // les mois sont numérotés de 0 à 11, 11 est décembre var month = 11; // les données enregistrées. // localStorage est un objet fourni par javascript (html 5) // Les données qu'on y met sont enregistrées dans le browser // et sont disponibles à chaque fois qu'on va sur la page. var horaires = localStorage["horaires"]; if(horaires){ return DAY.horairesToRecordConfigs(year,month,horaires) }else{ return DAY.makeEmptyRecordConfigs(year,month); }
}
// Remet tous les horaires de la liste à "-" function resetData(list){
var records = list.getStore().data; for(var i=0; i< records.length; i++){ // records est une MixedCollection, sur laquelle il faut appeler la méthode get. // record est un modèle. Pour le modifier il faut utiliser la méthode set // http://docs.sencha.com/touch/1-1/#!/api/Ext.data.Model records.get(i).set("horaire","-"); }
}
// Envoie les horaires sur un serveur. function postData(list){
// Array de tous les modèles Day var records = list.getStore().data.getRange(); // string "3s|4|r|..." var horaires = DAY.recordsToHoraires(records);
// On envoie les données sur le serveur par ajax. // On peut faire des requetes ajax directement en javascript // mais c'est plus confortable de passer par une librairie. // Ici nous utilisons celle qui est fournie par sencha. Ext.Ajax.request({
// l'adresse du serveur // J'y ai mis un programme php qui enregistre les données pour un nom. url: "http://infolipo.net/cours/data/data.php",
// C'est gentil de remplacer "andreas" par votre nom pour ne pas écraser mes données. // On peut voir ce qui est enregistré à la page // http://infolipo.net/cours/data/data.php?name=andreas params: {name:"andreas", data:horaires},
// Cette fonction est appelée au retour de la requête, si tout a bien marché success: function(response, opts) { alert("Réponse du serveur: " + response.responseText); },
// Cette fonction est appelée en cas d'échec. failure: function(response, opts) { alert('échec: ' + response.status); } });
}
// Les fonctions pour manipuler le modèle Day ont assez en commun // pour que je les regroupe dans un même objet, je trouve. // C'est une question de gout. var DAY = {
// Crée les configuration de modèles pour un mois // year: l'année // month: le mois // horaires: un string représentant les horaires "3s|4|r|..." horairesToRecordConfigs: function(year, month, horaires){ // les horaires sont enregistrés sous la forme "3s|4|r|..." // sépare les horaires pour avoir un array ["3s","4","r",...] var horairesEnArray = horaires.split("|"); var day = 0; return horairesEnArray.map(function(h){ var date = new Date(year,month,day); var config = DAY.createRecordConfig(h,date); day ++; return config; } ); },
// Crée un string qui représente les horaires "3s|4|r|..." // à partir d'un array de modèles Day recordsToHoraires: function(records){ // On tire des records un array qui contient les horaires dans l'ordre // par exemple ["3s","4","r",...] var horairesEnArray = records.map(function(record){ // C'est un modèle. Il faut utiliser la méthode get // http://docs.sencha.com/touch/1-1/#!/api/Ext.data.Model return record.get("horaire"); }); // retourner un string "3s|4|r|..." return horairesEnArray.join("|"); },
// Retourne un array de configurations pour le modèle Day, // toutes avec l'horaire "-", // pour tous les jours du mois indiqué makeEmptyRecordConfigs: function(year, month){ // prendre l'array de dates fourni par datesOfMonth() // et le transformer en array de configurations pour le modèle Day. var datesData = DAY.datesOfMonth(year,month).map( function(d){ return DAY.createRecordConfig("-",d);} ); return datesData; },
// Crée la configuration pour un modèle Day createRecordConfig: function(l_horaire, la_date){ // la méthode format est ajoutée à Date par sencha // voir http://docs.sencha.com/touch/1-1/#!/api/Date return { date: la_date.format("D d.m.Y"), horaire: l_horaire }; },
// Retourne un array contenant les objets Date du mois indiqué datesOfMonth: function(year, month){
var date = new Date(year, month, 1); var month = date.getMonth() var dates = []; date.setDate(1); while (month === date.getMonth()) { dates.push(date); // la méthode add est ajoutée par sencha // à la classe Date fournie par javascript date = date.add(Date.DAY, 1); } return dates;
}
}
</source>
exercices
Il s'agit en premier de décortiquer l'application pour comprendre ce quelle fait, et en suite de l'améliorer
Comprendre l'interface graphique
- Déplacer la barre des taches du bas de l'écran vers le haut
- Rajouter une rangée de boutons
Comprendre le fonctionnement de l'application
- Désactiver la fonctionnalité de chaque bouton et la remplacer par un alert.
- Voir comment fonctionne l'enregistrement de données.
- Voir comment l'application envoie des données ailleurs par ajax.
Améliorer l'interface graphique
- Créer une plus belle template pour afficher les rangées.
- Les boutons save, reset et post sont trop accessibles, ils pourraient être appuyés par erreur
- Ouvrir un dialogue de confirmation qui dit "vraiment?", avec un bouton Interrompre et un bouton OK qui déclenche l'action.
- Autre solution: remplacer les trois boutons par un bouton actions qui ouvre un panneau avec save, reset et post
Améliorer la fonctionnalité
- Ajouter un bouton pour appeler le syndicat
- L'application ne fonctionne que pour un mois de l'année. Comment peut-on laisser l'utilisateur choisir le mois
- Choisir un composant graphique
- Ajouter un bouton qui appelle ce composant graphique
- Gérer l'affichage en fonction du mois
- Gérer la nouvelle donnée du mois pour l'enregistrement et l'envoi par ajax.
- Les horaires sont identifiés par un nom, mais ils correspondent à des heures de début et de fin. Comment intégrer les heures dans l'application?
- Tenir compte des horaires coupés qui sont fait d'une période de travail le matin, et d'une autre le soir.
Améliorations hardcore
- Trouver comment faire pour que les boutons reflètent l'état du jour sélectionné. Je n'ai pas trouvé comment faire en sorte qu'un bouton reste appuyé. Je n'ai pas voulu utilisé les radiobuttons qui me semblaient trop volumineux. Est-ce qu'il faut créer des boutons soi-même avec html/css?
- Publier les horaires sous forme d'évènements dans un agenda en ligne google. Ce serait très utile parce que les agendas d'android et d'iphone peuvent être synchronisés avec un agenda google.
A part ça ce serait utile de profiter de html5 pour permettre à l'application de tourner offline: http://www.sencha.com/learn/taking-sencha-touch-apps-offline/