Editor de horario semanal

Escribo porque por tercera vez en un año me encuentro con esta tarea. Cada vez, todo comienza con una solución increíblemente creativa más fácil, y al final llega al sistema del que hablaré.

El objetivo es crear y mantener un horario semanal, como un horario de clases escolares o un horario de médicos y funcionarios. Hay un conjunto de ranuras, cada ranura es un lugar en el programa semanal con varios parámetros adicionales, como el número de gabinete, el nombre del empleado. Se requiere construir un sistema flexible con un historial completo que pueda resolver problemas tales como: crear otro horario desde el comienzo del verano, reemplazar al maestro durante las próximas 3 semanas, mover el horario de viernes a sábado debido a las vacaciones.

Escribiré sobre lo que generalmente tropiezan y cómo resolverlo, resolveré el problema de pintar la tira, y luego daré ejemplos de un backend simple en node / sequelize y terminaré con una interfaz simple en vue / vuex / vuetify / nuxt, donde puedes arrastrar todo con un mouse y ver como funciona

Los códigos se publican en github , desplegados aquí .



Cambios granulares


Hay una ranura, presentada de alguna manera en la base de datos. Necesita edición Por lo tanto, debe dibujar algún formulario con los campos y debajo del botón "guardar". Después de todo, generalmente todo se arregla así. Sin embargo, no en este caso. Considere la forma:


Al guardar, todos los datos de la ranura se actualizan, el historial se pierde. Intentemos agregar tal elemento:


De nuevo por. Por ejemplo, el 4 de junio, el lunes, se registró una transferencia de clases de un día de la primera oficina a la segunda. Luego viene una nueva demanda: a partir del 28 de mayo, la lección siempre comenzará a las 20:00 en lugar de las 19:00. Abrimos el formulario, cambiamos la hora, indicamos la fecha del 28 y para siempre y ... todos los campos, junto con el número del gabinete, van al servidor. El cambio temporal el 4 de junio se anula. Con este formulario, es imposible determinar qué campos a qué intervalos desea cambiar el usuario, porque todos los campos se envían.

La idea es que cada regla cambie independientemente de las demás con su propio intervalo. La ranura está definida por un conjunto de parámetros unidimensionales, cada parámetro tiene un historial de cambios definido por un conjunto de reglas. Cada regla contiene un valor, una fecha de inicio y finalización. Como se trata de un calendario semanal, las fechas son suficientes para indicar hasta una semana, AAAAWW.


Puede parecer que editar el espacio ahora es muy complicado: para cambiar varios campos, debe seleccionar cada campo, abrir el formulario, anotar un valor y un intervalo. Sin embargo, en la práctica, cambiar varios campos ha resultado ser una situación poco común. Mucho más frecuente es la actualización masiva de varias ranuras a la vez. Por ejemplo, para sofocar la ausencia de un maestro debido a una enfermedad, debe seleccionar todos sus bloques, colocar el estado de asignación del personal en licencia médica y luego seleccionar un maestro de reemplazo para los mismos bloques. Solo 2 acciones en lugar de n acciones para n ranuras en el caso, como si se especificaran a través del formulario tradicional. En el sistema StarBright.com en el que estoy trabajando actualmente, se ve así:


La tarea de pintar tiras


Considere una tira que consiste en celdas pintadas en diferentes colores. Cada celda es una semana, cada color es un significado. Llega un nuevo color y el intervalo en el cual aplicarlo, necesitan volver a pintar encima de lo que es. A nivel de datos, esto significa que debe eliminar los intervalos completamente superpuestos, cambiar los intervalos de superposición parcial, agregar un nuevo intervalo, fusionar los intervalos adyacentes de un color en uno. El resultado final debe consistir en intervalos que no se superpongan.


Resultado: [{delete, id: 2}, {update, id: 1, data: {to: 5}}, {update, id: 3, data: {from: 16}}, {insert, data: {from : 6, a: 15, valor: mié}}]

Esta es una tarea simple, pero es fácil ignorar algo aquí. Aquí hay un repositorio separado con una solución y pruebas. http://timeblock-rules.rag.lt : aquí puedes comprobar cómo funciona y jugar con el sombreado.

Backend


Las reglas no se superponen, por lo que el simple `seleccionar * de las reglas donde <=: semana y (to es nulo o to> =: week)` es suficiente para seleccionar exactamente las reglas necesarias para la semana especificada. Aquí hay un ejemplo simple de back-end en node / sequelize. Utiliza el estilo combinado de c promises y async / await, que puedes leer en otro artículo mío .

Aquí está la acción que selecciona las reglas para la semana especificada:
routes.get('/timeblocks', async (req, res) => { try { ... validation ... await Rule .findAll({ where: { from: {$or: [{$lte: req.query.week}, null]}, to: {$or: [{$gte: req.query.week}, null]} } }) .then( sendSuccess(res, 'Calendar data extracted.'), throwError(500, 'sequelize error') ) } catch (error) { catchError(res, error) } }) 


Y aquí está PATCH para cambiar el conjunto de reglas:
 routes.patch('/timeblocks/:id(\\d+)', async (req, res) => { try { ... validation ... const initialRules = await Rule .findAll({ where: { timeblock_id: req.params.id, type: {$in: req.params.rules.map(rule => rule.type)} } }).catch(throwError(500, 'sequelize error')) const promises = [] req.params.rules.forEach(rule => { // This function defined in stripe coloring repo, https://github.com/Kasheftin/timeblock-rules/blob/master/src/fn/rules.js; const actions = processNewRule(rule, initialRules[rule.type] || []) actions.forEach(action => { if (action.type === 'delete') { promises.push(Rule.destroy({where: {id: action.id}})) } else if (action.type === 'update') { promises.push(Rule.update(action.data, {where: {id: action.id}})) } else if (action.type === 'insert') { promises.push(Rule.build({...action.data, timeblock_id: rule.timeblock_id, type: rule.type}).save()) } }) }) Promise.all(promises).then( result => sendSuccess(res, 'Timeblock rules updated.')() ) } catch (error) { catchError(res, error) } }) 


Esta es la parte ideológica más difícil del backend, el resto es aún más simple.

La pregunta es cómo eliminar ranuras. En este caso, se almacena el historial completo, no se elimina nada. Hay un campo de estado que se puede abrir, cerrar temporalmente y cerrar. Los visitantes ven espacios activos y temporalmente inactivos, en este último, el administrador generalmente escribe un comentario sobre por qué no hay actividad. Con el tiempo, hay muchos espacios cerrados, y para simplificar la situación, es útil introducir otra propiedad, como un año escolar, y mostrar solo el año escolar actual al editar espacios.

Frontend


El código está en este repositorio , es un sitio simple de una página en nuxt. En realidad, hay algunos problemas con ssr (por ejemplo, analizo en detalle cómo escribir autenticación en nuxt), pero las aplicaciones simples se escriben en él muy rápidamente.

Aquí está el código para una sola página:
 export default { components: {...}, fetch ({app, route, redirect, store}) { if (!route.query.week) { const newRoute = app.router.resolve({query: {...route.query, week: moment().format('YYYYWW')}}, route) return redirect(newRoute.href) } return Promise.resolve() .then(() => store.dispatch('calendar/set', {week: route.query.week})) .then(() => store.dispatch('calendar/fetch')) }, computed: { week () { return this.$store.state.calendar.week } }, watch: { week (week) { this.$router.push({ query: { ...this.$route.query, week } }) this.$store.dispatch('calendar/fetch') } } } 


El método fetch funciona en el servidor y el cliente, redirige a la semana actual y solicita un calendario. Cuando cambia la semana, los datos se vuelven a solicitar.

¿Qué hacer con las ranuras superpuestas? La respuesta depende de la lógica del negocio, por ejemplo, puede necesitar la validación del servidor que prohíbe las superposiciones. En este caso, se permiten superposiciones y, para obtener una imagen hermosa, estas ranuras se dibujan la mitad del ancho una al lado de la otra. Agregue el diseño y obtenga este aspecto:



Todo lo demás es javascript sin ideas especiales. Al hacer clic con el mouse en el bloque, comienza a arrastrar y soltar. Los eventos mousemove y mouseup se cuelgan en toda la ventana. Arrastrar y soltar comienza con un retraso de 200 ms para distinguir el arrastre del clic. Los parámetros de los contenedores en los que se realiza un seguimiento de drop se calculan de antemano, porque getBoundingClientRect es una operación demasiado pesada para cada movimiento del mouse. Tuve que hacer dos formularios: uno para crear (establecer todas las reglas a la vez a partir de la semana actual), el otro para los cambios granulares en la ranura.

http://calendar.rag.lt : aquí puede comprobar cómo funciona todo.

Enlaces al articulo


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


All Articles