Horaires de ma femme

De Wikilipo.

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.

Sommaire

application.js

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;
 }
}

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

  1. Déplacer la barre des taches du bas de l'écran vers le haut
  2. Rajouter une rangée de boutons

Comprendre le fonctionnement de l'application

  1. Désactiver la fonctionnalité de chaque bouton et la remplacer par un alert.
  2. Voir comment fonctionne l'enregistrement de données.
  3. Voir comment l'application envoie des données ailleurs par ajax.

Améliorer l'interface graphique

  1. Créer une plus belle template pour afficher les rangées.
  2. Les boutons save, reset et post sont trop accessibles, ils pourraient être appuyés par erreur
    1. Ouvrir un dialogue de confirmation qui dit "vraiment?", avec un bouton Interrompre et un bouton OK qui déclenche l'action.
    2. Autre solution: remplacer les trois boutons par un bouton actions qui ouvre un panneau avec save, reset et post

Améliorer la fonctionnalité

  1. Ajouter un bouton pour appeler le syndicat
  2. L'application ne fonctionne que pour un mois de l'année. Comment peut-on laisser l'utilisateur choisir le mois
    1. Choisir un composant graphique
    2. Ajouter un bouton qui appelle ce composant graphique
    3. Gérer l'affichage en fonction du mois
    4. Gérer la nouvelle donnée du mois pour l'enregistrement et l'envoi par ajax.
  3. 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?
    1. 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

  1. 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?
  2. 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/

Outils personnels