J'écris parce que pour la troisième fois en un an je tombe sur cette tâche. À chaque fois, tout commence par une solution incroyablement créative plus simple, et au final arrive le système dont je vais parler.
L'objectif est de créer et de maintenir un horaire hebdomadaire, tel qu'un horaire de cours à l'école ou un horaire de médecins et d'officiels. Il y a un ensemble de créneaux horaires, chaque créneau est un emplacement dans le calendrier hebdomadaire avec divers paramètres supplémentaires, tels que le numéro d'armoire, le nom de l'employé. Il est nécessaire de construire un système flexible avec une histoire complète qui peut résoudre des problèmes tels que: créer un autre horaire à partir du début de l'été, remplacer l'enseignant pour les 3 prochaines semaines, déplacer l'horaire du vendredi au samedi en raison des vacances.
Je vais écrire sur ce qu'ils trébuchent habituellement et comment le résoudre, je vais résoudre le problème de la peinture de la bande, puis je vais donner des exemples d'un simple backend sur node / sequelize et terminer par un simple frontend sur vue / vuex / vuetify / nuxt, où vous pouvez faire glisser le tout avec une souris et voir comment ça marche
Les codes sont affichés sur
github , déployé
ici .

Changements granulaires
Il y a une fente, en quelque sorte présentée dans la base de données. Besoin d'édition. Vous devez donc dessiner un formulaire avec les champs, et sous le bouton "enregistrer". Après tout, tout est généralement arrangé comme ça. Cependant, pas dans ce cas. Considérez le formulaire:
Lors de l'enregistrement, toutes les données de l'emplacement sont mises à jour, l'historique est perdu. Essayons d'ajouter un tel élément:
Encore une fois par. Par exemple, le 4 juin, lundi, un transfert d'une journée de cours du premier bureau au deuxième a été enregistré. Vient ensuite une nouvelle demande - à partir du 28 mai, la leçon commencera toujours à 20h00 au lieu de 19h00. Nous ouvrons le formulaire, modifions l'heure, indiquons la date du 28 et pour toujours et ... tous les champs, ainsi que le numéro d'armoire, vont au serveur. Le changement temporaire du 4 juin est annulé. En utilisant ce formulaire, il est impossible de déterminer quels champs à quels intervalles l'utilisateur veut changer, car tous les champs sont envoyés.
L'idée est que chaque règle change indépendamment des autres avec son propre intervalle. Le créneau est défini par un ensemble de paramètres unidimensionnels, chaque paramètre a un historique des modifications défini par un ensemble de règles. Chaque règle contient une valeur, une date de début et de fin. Puisqu'il s'agit d'un calendrier hebdomadaire, les dates sont suffisantes pour indiquer jusqu'à une semaine, YYYYWW.

Il peut sembler que la modification de l'emplacement est désormais très compliquée - pour modifier plusieurs champs, vous devez sélectionner chaque champ, ouvrir le formulaire, noter une valeur et un intervalle. Cependant, dans la pratique, le changement de plusieurs domaines s'est révélé être une situation rare. La mise à jour en masse de plusieurs emplacements à la fois est beaucoup plus fréquente. Par exemple, pour supprimer l'absence d'un enseignant en raison d'une maladie, vous devez sélectionner tous ses blocs, mettre le statut d'affectation du personnel en congé de maladie, puis sélectionner un enseignant remplaçant pour les mêmes blocs. Seulement 2 actions au lieu de n actions pour n emplacements dans le cas, comme si elles étaient spécifiées via le formulaire traditionnel. Sur le système
StarBright.com sur lequel je travaille actuellement, cela ressemble à ceci:
La tâche de peindre des bandes
Considérons une bande composée de cellules peintes de différentes couleurs. Chaque cellule est une semaine, chaque couleur a un sens. Une nouvelle couleur arrive et l'intervalle dans lequel l'appliquer, ils doivent repeindre au-dessus de ce qui est. Au niveau des données, cela signifie que vous devez supprimer les intervalles qui se chevauchent complètement, modifier les intervalles de ceux qui se chevauchent partiellement, ajouter un nouvel intervalle, fusionner les intervalles adjacents d'une seule couleur en un seul. Le résultat final doit être composé d'intervalles qui ne se chevauchent pas.
Résultat: [{delete, id: 2}, {update, id: 1, data: {to: 5}}, {update, id: 3, data: {from: 16}}, {insert, data: {from : 6, à: 15, valeur: mer}}]C'est une tâche simple, mais il est facile d'ignorer quelque chose ici.
Voici un référentiel séparé avec une solution et des tests.
http://timeblock-rules.rag.lt - ici vous pouvez vérifier comment cela fonctionne et jouer avec l'ombrage.
Backend
Les règles ne se chevauchent pas, donc le plus simple `sélectionner * parmi les règles où <=: semaine et (à est nul ou à> =: semaine) 'est suffisant pour sélectionner exactement les règles nécessaires pour la semaine spécifiée.
Voici un exemple simple de backend sur node / sequelize. Il utilise le style combiné de promesses c et async / wait, que vous pouvez lire
dans un autre article .
Voici l'action qui sélectionne les règles pour la semaine spécifiée:
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) } })
Et voici PATCH pour changer le jeu de règles:
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 => {
C'est la partie idéologique la plus difficile du backend, le reste est encore plus simple.
La question est de savoir comment supprimer les emplacements. Dans ce cas, l'historique complet est stocké, rien n'est supprimé. Il y a un champ d'état qui peut être ouvert, fermé temporairement et fermé. Les visiteurs voient des créneaux actifs et temporairement inactifs, dans ce dernier, généralement l'administrateur écrit généralement un commentaire expliquant pourquoi il n'y a pas d'activité. Au fil du temps, il y a beaucoup de créneaux fermés, et afin de simplifier la situation, il est utile d'introduire une autre propriété telle qu'une année scolaire et d'afficher uniquement l'année scolaire en cours lors de la modification des créneaux.
Frontend
Le code est dans
ce référentiel , c'est un simple site d'une page sur nuxt. En fait, il y a quelques problèmes avec ssr (par exemple, j'analyse en détail comment écrire l'authentification sur nuxt), mais des applications simples y sont écrites très rapidement.
Voici le code d'une seule page:
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') } } }
La méthode de récupération fonctionne sur le serveur et le client, redirige vers la semaine en cours et demande un calendrier. Lorsque la semaine change, les données sont redemandées.
Que faire avec des créneaux qui se chevauchent? La réponse dépend de la logique métier, par exemple, vous pouvez avoir besoin d'une validation de serveur qui interdit les superpositions. Dans ce cas, les superpositions sont autorisées et pour obtenir une belle image, ces fentes sont dessinées la moitié de la largeur les unes à côté des autres. Ajoutez la mise en page et obtenez ce look:

Tout le reste est du javascript simple sans idées spéciales. En mousedown sur le bloc, le glisser-déposer commence. Les événements mousemove et mouseup sont suspendus sur toute la fenêtre. Le glisser-déposer commence avec un retard de 200 ms afin de distinguer le glisser-déplacer du clic. Les paramètres des conteneurs dans lesquels la chute est suivie sont calculés à l'avance, car getBoundingClientRect est une opération trop lourde à effectuer pour chaque déplacement de souris. J'ai dû faire deux formulaires - un pour la création (définition de toutes les règles à la fois à partir de la semaine en cours), l'autre pour des modifications granulaires de l'emplacement.
http://calendar.rag.lt - ici vous pouvez vérifier comment tout fonctionne.
Liens vers l'article