Weekly schedule editor

I am writing, because for the third time in a year I come across this task. Every time everything starts with a surprisingly creative solution easier, and in the end comes to the system, which I will tell.

The task is to create and maintain a weekly schedule, such as a school lesson schedule or a schedule of doctors and officials. There is a set of slots, each slot is a place in the weekly schedule with various additional parameters, such as the office number, the name of the employee. It is required to build a flexible system with a complete history, capable of solving tasks like: create a different schedule from the beginning of summer, replace the teacher for the next 3 weeks, move the schedule from Friday to Saturday because of the holiday.

I'll write about what they usually stumble and how to solve it, solve the problem of painting the strip, and then give examples of a simple backend on node / sequelize and finish it with a simple frontend on vue / vuex / vuetify / nuxt, where you can drag it all over and see How does it work.

The codes are posted on github , deployed here .



Granular changes


There is a slot, somehow represented in the database. Need editing. So you need to draw some form with fields, and at the bottom a "save" button. After all, usually everything is arranged. However, not in this case. Consider the form:


When saving, all slot data is updated, the history is lost. Let's try to add such an element:


Again by. For example, on the 4th of June, on Monday, a one-day move was recorded from the first classroom to the second class. Then comes the new requirement - from May 28, the lesson will always begin at 20:00 instead of 19:00. We open the form, change the time, specify the date from the 28th and forever and ... all the fields, together with the account number, go to the server. A temporary change on June 4th is overwritten. According to this form, it is impossible to determine which fields at which intervals the user wants to change, because in general all the fields are sent.

The idea is that each rule changes independently of the others with its own interval. A slot is defined by a set of one-dimensional parameters; each parameter has a change history defined by a set of rules. Each rule contains a value, a start and end date. Since this is a weekly calendar, it is sufficient to indicate the dates with the accuracy of a week, YYYYWW.


It may seem that slot editing is now very complicated - to change several fields, you need to select each field, open a form, set a value and an interval. However, in practice, changing several fields turned out to be a rare situation. Much more frequent - bulk-update several slots at a time. For example, to record the absence of a teacher due to illness, you need to select all of its blocks, put down the assignment status in medical leave, and then select the replacement teacher for the same blocks. Only 2 actions instead of n actions for n slots in the case, as in the case if they were specified through the traditional form. On the StarBright.com system I'm working on, it looks like this:


The task of painting the strip


Consider the strip, consisting of cells, painted in different colors. Every cell is a week, every color is a value. A new color comes in and the interval in which to apply it, they need to repaint over what is. At the data level, this means that you need to remove completely overlapped intervals, change the intervals partially overlapped, add a new interval, merge adjacent monochrome intervals into one. The final result should consist of intervals that do not overlap.


Result: [{delete, id: 2}, {update, id: 1, data: {to: 5}}, {update, id: 3, data: {from: 16}}, {insert, data: {from : 6, to: 15, value: wed}}]

This is a simple task, but it’s easy to ignore something. Here is a separate repository with solutions and tests. http://timeblock-rules.rag.lt - here you can check how it works and play with painting.

Backend


The rules do not overlap, so the simplest `select * from rules where from <=: week and (is null or to> =: week)` is enough to select exactly the rules you need for the specified week. Here is a simple example of a node / sequelize backend. It uses the combined style of c promises and async / await, which can be read about in my other article .

Here is the action that selects the rules for the specified week:
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) } }) 


And here is the PATCH for changing the rule set:
 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) } }) 


This is the most difficult ideological part of the backend, the rest is even simpler.

The question arises how to remove slots. In this case, the full history is stored, nothing is deleted. There is a status field that can be opened, temporary closed and closed. Visitors see active slots and are temporarily inactive, on the latter the admin usually usually writes a comment why there is no activity. Closed-slots over time becomes a lot, and, to simplify the situation, it is useful to introduce another property of the type of school year, show when editing slots of only the current school year.

Frontend


The code is in this repository , this is a simple one-page site on nuxt. In fact, there are several problems with ssr (for example, I analyze in detail how to write auth-authentication on nuxt), but simple applications are written very quickly on it.

Here is the code for a single 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') } } } 


The fetch method works on the server and client, redirects for the current week and requests a calendar. When you change the week is a re-request data.

What to do with overlapping slots? The answer depends on the business logic, for example, validation may be required on the server, which prohibits overlaying. In this case, overlaps are allowed, and to get a beautiful picture, such slots are drawn half the width next to each other. Add the layout and get this look:



Everything else is plain javascript without any special ideas. By mousedown on the block starts dragging. The mousemove and mouseup events are hung on the entire window. Dragging begins with a delay of 200ms in order to distinguish drag from a click. The parameters of containers that are monitored for drop are pre-calculated, because getBoundingClientRect is too heavy to perform on each mousemove. I had to make two forms - one to create (setting all the rules at a time starting from the current week), another - for granular slot changes.

http://calendar.rag.lt - here you can check how everything works.

Links to the article


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


All Articles