Cómo mejoramos el orden del almuerzo en la oficina (sin acceso al servidor)

Hola a todos

Yo trabajo en una oficina. Desarrollador de software. Y a veces como. Si todos los dias. El empleador nos proporciona almuerzos: los trabajadores ordenan el almuerzo para mañana y mañana el proveedor de almuerzos trae lo que los empleados ordenaron. Lo que se ordenó y lo que se trajo no siempre coincide, pero este no es el caso. El almuerzo se ordena en la página de orden de almuerzo. Pero ...

Pero primero, sobre cómo se forma la página de pedido de almuerzo: el proveedor envía un archivo XLS con una lista de precios durante una semana.

imagen
Ejemplo de lista de precios enviada por el proveedor

La persona responsable de las cenas analiza a través de una utilidad desarrollada por alguien en las entrañas de nuestra empresa, traduciéndola en una forma que nuestro portal corporativo puede mostrar. Y lo muestra ...

imagen
Captura de pantalla con cenas ordenadas

imagen
Captura de pantalla de la página de pedidos de almuerzo.

Las posiciones se dividen incómodamente en categorías. La información sobre el nombre y la composición está en texto completo y es difícil de navegar.

Quiero entender que es mejor no ordenar, y lo que puedes probar, porque a otros les gusta. Es decir, quiero una calificación. También quiero recibir mi pedido en Telegram para no recordar lo que ordené en el comedor.

Entonces los objetivos son claros. Debo decir de inmediato: el camino que mi colega y yo hemos tomado está lejos de ser el más correcto y racional. Aun así: es un juego completo en términos de arquitectura / seguridad / soporte / tolerancia a fallos. Pero lo que ha crecido ha crecido.

No tenemos acceso al servidor, por lo que solo puede cambiar la apariencia de la página con scripts de usuario. ¿Pero qué hay de la calificación? Tampoco hay acceso a la base de datos. Bueno, necesitamos un servidor para el procesamiento de pedidos, calificación e interacción con Telegram. Esta función fue tomada por el servidor NodeJS.

Lado del servidor


Yo me ocuparé del servidor, y un colega se encargará de un script de usuario que agrega funcionalidad a la página. Tomamos el servidor nodejs, conectamos express, agregamos MySQL. Poner Sequelize en la parte superior . E interactuaremos con Telegram a través de 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); 

Brevemente sobre la funcionalidad:
La ruta / dinners / user_menu devuelve un script de usuario:

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

Esto se hace para no distraer a los colegas que lo usan instalando una nueva versión del script. Se corrigió, lo lanzó al servidor, todos se actualizaron.

Sí, sé que desde un punto de vista de seguridad esto es malo, pero la funcionalidad en sí misma no es crítica y consideraremos que el servidor en el que se almacena el script es bastante seguro.

Además, a lo largo de la ruta / dinners / r /: id, puede obtener una calificación para todas las posiciones y guardar la calificación, es decir, votar por los platos.

La ruta / dinners / reenviar /: id sirve para enviar un mensaje a Telegram. El texto del mensaje se genera en el cliente, solo se produce interacción con Telegram en el servidor:

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

Después de eso, el Bot envía un mensaje con la orden.

imagen

A continuación, a lo largo de la ruta / cenas / orden , se guarda la orden . Dado que la solicitud de pedido original es difícil de determinar (después de hacer clic en el botón "Guardar", aparece una alerta con el botón de confirmación de pedido), se envía una solicitud al servidor con pedidos cuando se carga la página de pedidos (y todo el sistema de pedidos en el sitio se divide en 2 páginas: la página de pedidos y la página de menú - selección de platos para un día específico, es decir, la formación del pedido). No es racional enviar solicitudes cada vez que ingresa a la página de pedido, pero no había una mejor opción para un complemento.

Finalmente, la ruta / dinners / days establece los días para ordenar el almuerzo. Esta parte de la funcionalidad ha aparecido para el correcto funcionamiento de los recordatorios sobre un pedido inacabado: debe saber cuál es el próximo día del pedido (hay fines de semana y feriados a mediados de la semana). En lugar de tomar la implementación del calendario de producción, simplemente analizo las fechas en la página de pedido, donde los días laborables y no laborables ya están marcados (no puede hacer un pedido para un día no laborable). Los días no laborables están marcados en el portal con la clase 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 sí, usa jquery para recoger. Es muy conveniente profundizar en el árbol de páginas.

Telegram bot


Otra parte de todo el complemento es el bot de telegramas.

imagen
Con tal funcionalidad

Obtener una identificación es un sistema de identificación de este tipo. Para asociar un script de usuario en un navegador específico con userId en telegrama.

Vea el pedido de hoy, vea la lista de pedidos (últimos 5), establezca un recordatorio.
El almuerzo se envía automáticamente al proveedor a la misma hora todos los días, por lo que es importante realizar un pedido antes de una determinada hora, por ejemplo, a las 13:00.

Después de eso, se bloquea la capacidad de realizar un pedido.

Recordatorios:

imagen
El bot brinda la oportunidad de elegir un recordatorio: 9, 10 u 11 horas.

Además, si después de un recordatorio no realizó un pedido, cada 10 minutos siguientes el bot le recordará el pedido hasta que lo solicite o hasta que el pedido esté bloqueado.

Esto se hace mediante la tarea cron (usando node-schedule ):

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

Parte del cliente. Menu


Repito que la interfaz actual junto con el texto de los elementos del menú que envía el proveedor es simplemente horrible (ver pantalla 2). Y en un momento, dejas de ver nada en toneladas de texto monótono sólido y poco útil.

Después de buscar a través de Internet que podría ayudarnos, encontramos un complemento bastante bueno para los scripts personalizados de Greasemonkey, y decidieron usarlo.

En primer lugar, creamos un script de usuario y le damos el derecho de comunicarse con el portal corporativo y el servidor, en el que se atornillan la calificación y la capacidad de enviar solicitudes

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

Además, para modificar la página del almuerzo, usamos jQuery, conectándolo usando // @require

Ahora comencemos palear la página del almuerzo. Después de mirar el código html de la página, encontramos el identificador de la mesa del almuerzo, obtenemos la tabla y la modificamos.

 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>"); 

Quiero señalar que en la página de formación del almuerzo, al calcular el monto del pedido, se considera en todas las filas de la tabla, recibiendo el número del artículo pedido multiplicado por el precio. Por estas razones, si agrega una fila con el nombre de la categoría, todo se romperá ... Tuve que ingresar columnas ocultas con cantidad y cantidad cero para esta fila.

Ahora pasemos a limpiar el texto y agregar información sobre la calificación del plato. Primero, algunas funciones auxiliares. El plato en la clasificación se identifica por su nombre sin basura en forma de gramos, puntuación y espacios. Es decir, un plato llamado “Caldo de pollo con huevo (caldo de pollo, zanahorias, cebollas, huevos, verduras). En 100 g: proteínas-3.43; grasas-2.86; carbohidratos-1.0; en.value-43.39kcal (200gr) "se identifica como" cuajada de caldo ". Esto se debe al hecho de que el proveedor puede arrastrarse en espacios adicionales, letreros y algo más. Como la práctica ha demostrado, esto fue suficiente para identificar con precisión el plato en el 90% de los casos, y decidimos no molestarnos e ingresar una búsqueda de texto completo.

 /** *         * @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) { //   ,       } 

Y esta es la formación de una calificación:

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

Y eso es lo que pasó.

imagen
De acuerdo, mucho más agradable y más conveniente?

Parte del cliente. Votar


A continuación, pasaremos a agregar la capacidad de votar por los platos ordenados, así como a enviar un mensaje con la orden a Telegram.

imagen
Página con pedidos sin guión

En la página de platos ordenados, agregue la calificación:

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

Y esto es lo que obtuvimos en la salida:

imagen

Sí, el código es terrible. Sí, no optimizado. Y sí, en algunos lugares ilógico. Pero el tiempo empleado fue mínimo, y la funcionalidad y la conveniencia aumentaron significativamente.

El objetivo era hacer que ordenar la cena fuera más agradable para mí y para mis camaradas, y este objetivo, en mi opinión, se logró.

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


All Articles