Comment nous avons amélioré l'ordre du déjeuner au bureau (sans accès au serveur)

Bonjour à tous.

Je travaille dans un bureau. Développeur logiciel. Et parfois je mange. Oui, tous les jours. L'employeur nous fournit les déjeuners - les ouvriers commandent le déjeuner pour demain, et demain le fournisseur de déjeuners apporte ce que les ouvriers ont commandé. Ce qui a été commandé et ce qui a été apporté ne coïncide pas toujours, mais ce n'est pas le cas. Le déjeuner est commandé sur la page de commande du déjeuner. Mais ...

Mais d'abord, sur la façon dont la page de commande du déjeuner est formée: le fournisseur envoie un fichier XLS avec une liste de prix pour une semaine.

image
Exemple de liste de prix envoyée par le fournisseur

La personne responsable des dîners analyse un utilitaire développé par quelqu'un dans les entrailles de notre entreprise, le traduisant sous une forme que notre portail d'entreprise peut afficher. Et il l'affiche ...

image
Capture d'écran avec dîners commandés

image
Capture d'écran de la page de commande du déjeuner

Les postes sont inconfortablement divisés en catégories. Les informations sur le nom et la composition sont en texte intégral et il est difficile de s'y retrouver.

Je veux comprendre qu'il vaut mieux ne pas commander, et ce que vous pouvez essayer, parce que d'autres l'aiment. Autrement dit, je veux une note. Je souhaite également recevoir ma commande sur Telegram afin de ne pas me souvenir de ce que j'ai commandé dans la salle à manger.

Les objectifs sont donc clairs. Je dois dire tout de suite: le chemin que mon collègue et moi avons pris est loin d'être le plus correct et le plus rationnel. Malgré tout: c'est un jeu complet en termes d'architecture / sécurité / support / tolérance aux pannes. Mais ce qui a grandi a grandi.

Nous n'avons pas accès au serveur, vous ne pouvez donc modifier l'apparence de la page qu'avec des scripts utilisateur. Mais qu'en est-il de la note? Il n'y a pas non plus accès à la base de données. Eh bien, nous avons besoin d'un serveur pour le traitement des commandes, l'évaluation et l'interaction avec Telegram. Ce rôle a été pris par le serveur NodeJS.

Côté serveur


Je m'occuperai du serveur, et un collègue s'occupera d'un script utilisateur qui ajoute des fonctionnalités à la page. Nous prenons le serveur nodejs, connectons express, ajoutons MySQL. Mettez Sequelize au sommet . Et nous interagirons avec Telegram via node-telegram-bot-api :

//    const app = express(); // ... //   //    app.get("/dinners/user_menu", dinner.getUserMenu); //      app.get("/dinners/r/:id", dinner.getPersonalRatings); //   app.post("/dinners/r/:id", dinner.setRating); //      Telegram app.post("/dinners/resend/:id", dinner.resendMessage); //      app.post("/dinners/order", dinner.order); //  ,      app.post("/dinners/days", dinner.setDinnerDays); 

En bref sur la fonctionnalité:
Le chemin / dinners / user_menu renvoie un script utilisateur:

 res.sendFile(__dirname + '/public_html/user_script.js'); 

Ceci est fait afin de ne pas distraire les collègues qui l'utilisent en installant une nouvelle version du script. Corrigé - l'a jeté sur le serveur - tout le monde a été mis à jour.

Oui, je sais que d'un point de vue de la sécurité, c'est mauvais, mais la fonctionnalité elle-même n'est pas critique et nous considérerons que le serveur sur lequel le script est stocké est assez sécurisé.

De plus, le long du chemin / dîners / r /: id, vous pouvez obtenir une note pour toutes les positions et enregistrer la note, c'est-à-dire voter pour les plats.

Le chemin / dîners / renvoyer /: id sert à envoyer un message à Telegram. Le texte du message est généré sur le client, seule l'interaction avec Telegram se produit sur le serveur:

 const parseMode: TelegramBot.SendMessageOptions = {parse_mode: "HTML"}; await this.bot.sendMessage(telegramId, htmlMessage, {...options, ...parseMode}); 

Après cela, le Bot envoie un message avec la commande.

image

Ensuite, le long du chemin / dîners / commande , la commande est enregistrée. Comme la demande de commande d'origine est difficile à déterminer (après avoir cliqué sur le bouton "Enregistrer", une alerte apparaît avec le bouton de confirmation de commande), la demande pour le serveur avec les commandes est envoyée lorsque la page de commande est chargée (et l'ensemble du système de commande sur le site est divisé en 2 pages - la page de commande et la page de menu - sélection de plats pour une journée spécifique - c'est-à-dire la formation de la commande). Il n'est absolument pas rationnel d'envoyer des demandes chaque fois que vous accédez à la page de commande, mais il n'y avait pas de meilleure option pour un instantané.

Enfin, le chemin / dîners / jours définit les jours pour commander le déjeuner. Cette partie de la fonctionnalité est apparue pour le bon fonctionnement des rappels sur une commande inachevée - vous devez savoir quel est le lendemain de la commande (après tout, il y a des week-ends et des jours fériés au milieu de la semaine). Au lieu de prendre l'implémentation du calendrier de production, j'analyse simplement les dates sur la page de commande, où les jours ouvrés et non ouvrés sont déjà marqués (vous ne pouvez pas passer commande pour un jour non ouvrable). Les jours chômés sont marqués sur le portail avec la classe isHoliday:

 //     const trToday = $(".dinner_today")[0]; const tbodyAllDays = $(trToday).parent(); const dinnerDays = []; $(tbodyAllDays).children().each(async function() { if ($(this).hasClass("isHoliday")) { return; } const itemMenuDate = $(this).find("> td:first-child").text().substring(0, 10); dinnerDays.push(itemMenuDate); // ... }); await sendRequest("POST", `https://****/dinners/days/`, {days: dinnerDays}); 

Oh oui, utilisez jquery pour la cueillette. Il est très pratique de se plonger dans l'arborescence des pages.

Télégramme bot


Une autre partie de l'ensemble complémentaire est le bot de télégramme.

image
Avec une telle fonctionnalité

L'obtention d'une pièce d'identité est un tel système d'identification. Pour associer un script utilisateur sur un navigateur spécifique à userId dans le télégramme.

Voir la commande d'aujourd'hui, voir la liste des commandes (5 dernières), définir un rappel.
Le déjeuner est automatiquement envoyé au fournisseur à la même heure chaque jour, il est donc important de passer une commande avant une certaine heure, disons 13h00.

Après cela, la possibilité de passer une commande est bloquée.

Rappels:

image
Le bot permet de choisir une heure de rappel: 9, 10 ou 11 heures.

De plus, si après un rappel vous n'avez pas passé de commande, toutes les 10 minutes, le bot vous rappellera la commande jusqu'à ce que vous la commandiez, ou jusqu'à ce que la commande soit bloquée.

Cela se fait par la tâche cron (en utilisant node-schedule ):

 schedule.scheduleJob('*/10 9-13 * * 1-5', async function() { // ... }); 

Partie client. Le menu


Je répète que l'interface actuelle en conjonction avec le texte des éléments de menu que le fournisseur envoie est tout simplement horrible (voir écran 2). Et à un moment donné, vous arrêtez de voir quoi que ce soit en tonnes de texte monotone solide et peu utile.

Après avoir cherché sur Internet qui pourrait nous aider, nous sommes tombés sur un assez bon plugin pour les scripts Greasemonkey personnalisés, et ils ont décidé de l'utiliser.

Tout d'abord, nous créons un script utilisateur et donnons le droit de communiquer avec le portail et le serveur de l'entreprise, sur lesquels la note et la capacité d'envoyer des demandes sont fixées

 // @include http://****.int/* // @include http://****/* // @grant GM.xmlHttpRequest 

De plus, pour modifier la page du déjeuner elle-même, nous avons utilisé jQuery, en la connectant à l'aide de // @require

Commençons maintenant à pelleter la page du déjeuner. Après avoir regardé le code html de la page, nous trouvons l'identifiant de la table du déjeuner, nous récupérons la table et la modifions.

 const table = $(".dinner__innerData"); const categoryList = []; //      $(table).find(“tbody tr td:nth-child(2})”).each(function () { const text = $(this).text(); //           if (!categoryList.find(name => name === text)) { $(this).parent().before("<tr><th colspan='6'>" + text + "</th><th style='display:none'></th><th style='display:none'></th><th style='display:none'>0</th><th style='display:none'><span class='dish__amount'>0</span></th></tr>"); categoryList.push(text); } }); //      $(table).find(“thead th:nth-child(2)”).remove(); $(table).find("tbody tr td:nth-child(2)”).remove(); //     $(table).find(“tbody tr td:nth-child(2)”).after("<td></td>"); $(table).find(“thead th:nth-child(2)”).after("<th class='ui-state-default'></th>"); 

Je tiens à noter que sur la page de formation du déjeuner, lors du calcul du montant de la commande, il est considéré sur toutes les lignes du tableau, recevant le numéro de l'article commandé multiplié par le prix. Pour ces raisons, si vous ajoutez une ligne avec le nom de la catégorie, tout se cassera ... J'ai dû entrer des colonnes cachées avec une quantité et un montant nul pour cette ligne.

Passons maintenant au nettoyage du texte et à l'ajout d'informations sur la note du plat. Tout d'abord, certaines fonctions d'assistance. Le plat dans la notation est identifié par son nom sans aucune ordure sous forme de grammes, de ponctuation et d'espaces. C'est-à-dire un plat appelé «bouillon de poulet aux oeufs (bouillon de poulet, carottes, oignons, œufs, légumes verts). Dans 100g: protéines-3,43; graisses-2,86; glucides-1,0; en.value-43.39kcal (200gr) »est identifié comme« bouillon caillé ». Cela est dû au fait que le fournisseur peut se glisser dans des espaces supplémentaires, des panneaux et autre chose. Comme la pratique l'a montré, cela a suffi pour identifier avec précision le plat dans 90% des cas, et nous avons décidé de ne pas déranger et d'entrer dans une recherche en texte intégral.

 /** *         * @param items   * @param tdText    * @return   */ function findByName(items, tdText) { tdText = clearTrash(tdText, true, true, true); return items.find(({clear_name}) => { return clear_name.trim().toLowerCase() === tdText; }); } /** *     * @param text  * @param clearDescr       * @param clearGrams    * @return    */ function clearTrash(text, clearDescr, clearGrams, clearSymbols) { //   ,       } 

Et c'est la formation d'une note:

 const table = $(".dinner__innerData"); const nameTd = $(table).find(“tr td:nth-child(2)”); for (let index = 0; index <= nameTd.length; index++) { const tdText = $(nameTd[index]).text(); //     const item = findByName(items, tdText); if (item) { let ratingTd = $(nameTd[index]).parent().find(“td:nth-child(2)”)[0]; //           let ratingText = "<i></i> " + parseFloat(item.avgrating).toFixed(1) + " (: " + item.orders + ", : " + item.ratingsCount + ")"; ratingText = item.persrating ? `<b><i></i> ${parseFloat(item.persrating).toFixed(1)} (: ${item.perscount})</b><br>` + ratingText : ratingText; //   $(ratingTd).css({ // getColorRating       background: getColorRating(item.avgrating) }).html(ratingText); } //           //   ,      const grams = getGrams(tdText); //     $(nameTd[index]).html(clearTrash(tdText, false, true, false)); //      ,       $(nameTd[index]).append("<br/><span></span>") .find("span") .append(grams) .css({"font-size": 10}); } 

Et c'est ce qui s'est passé.

image
D'accord, beaucoup plus agréable et plus pratique?

Partie client. Vote


Ensuite, nous allons ajouter la possibilité de voter pour les plats commandés, ainsi que d'envoyer un message avec la commande au télégramme.

image
Page avec des commandes sans script

Sur la page des plats commandés, ajoutez la note:

 async function addRatingForm() { const table = $(".dinner__innerData"); const nameTd = $(table).find("tr td:nth-child(1)"); //   for (let index = 0; index <= nameTd.length; index++) { const tdText = $(nameTd[index]).text(); $(nameTd[index]).html(clearTrash(tdText, false, true, false)); } //       Telegram $(table).append("<tfoot><tr><th colspan='6' class='rating-buttons btn-group margT0' style='display: table-cell;'></tr></tfoot>"); $(".rating-buttons").prepend(`<input type="submit" value="" class="btn_primary rating-button">`); $(".rating-buttons").prepend(`<input type="submit" value=" Telegram" class="btn_primary send-button">`); //      await diableButtonByDate(); //      for (let index = 0; index <= table.length; index++) { $(table[index]).find("tbody tr td:nth-child(4)").after("<td class='ratingInputTd'><input id='horizontal-spinner' class='ui-spinner-input' style='width:20px;'></td>"); $(table[index]).find("thead th:nth-child(4)").after("<th class='ui-state-default'></th>"); } $(".ui-spinner-input").spinner({ max: 10, min: 1 }); //   $(".rating-button").click(sendRating); $(".send-button").click(sendTelegram); } /** *      ,    */ async function diableButtonByDate() { //             . //              const buttons = $(".rating-button"); for (let index = 0; index <= buttons.length; index++) { const button = $(buttons[index]); const date = button.parent().parent().parent().parent().parent().parent().find("> td:nth-child(1)").text().substring(0, 10); if (await GM.getValue(date)) { button.attr({disabled: "disabled"}); } } } /** *  */ async function sendRating(event) { event.preventDefault(); const items = []; //         $(this).parent().parent().parent().parent().find("tr").each(function () { const tdList = $(this).find("td"); const ratingInput = $(tdList[4]).find("input"); if (!ratingInput.length) { return; } items.push({ count: $(tdList[2]).text(), price: $(tdList[1]).text(), name: $(tdList[0]).text(), rating: ratingInput.val(), }); }); await sendRequest("POST", `https://****/dinners/r/${telegramId}`, items); const menuDate = $(this).parent().parent().parent().parent().parent().parent().find("> td:nth-child(1)").text().substring(0, 10); await GM.setValue(menuDate, true); location.reload(); } 

Et voici ce que nous avons obtenu à la sortie:

image

Oui - le code est terrible. Oui - pas optimisé. Et oui - dans certains endroits illogiques. Mais le temps passé était au minimum, et la fonctionnalité et la commodité ont considérablement augmenté.

L'objectif était de rendre la commande du dîner plus agréable pour moi et mes camarades, et cet objectif, à mon avis, a été atteint.

Source: https://habr.com/ru/post/fr475892/


All Articles