Weekly schedule editor concept

Alexey Kuznetsov
codeburst
Published in
7 min readMay 28, 2018

--

I’m writing it because it seems to be rather popular task that has not any well known common solution. Usually it all starts with some surprisingly strange `simple` solution that by the end transforms to the system I’m going to write about.

The task is to create and support weekly schedule like lessons in a school or some medical institution staff work shifts. Basically, we have a bunch of slots, each one represents some day/time range in a week schedule with various additional options like the cabinet number, assigned staff, enrolled students, etc. We are trying to build flexible system with full history that is able to perform wide range of tasks such as filling the different schedule since summer, replace one teacher with another for the next 3 weeks or move the group from one room to another for the next Friday.

I’ll write about the common obstacle people usually meet while solving this task, then represent the basic idea, solve stripe coloring task and put it to SQL. Then I’ll give an example of simple back end using node/sequelize and finish with one page front end app on vue/vuex/vuetify/nuxt where anyone can drag&drop slots and see how it works.

All sources pulled to the github, the final result deployed here.

The Idea: separate rules per type

We have a slot, somehow represented in a database. We want to edit it. Then we should start with some form with a bunch of per property fields and some buttons ‘save’ below. That’s how all web usually works, but that’s not our case. Consider this form:

Clicking on save button will change all slot fields and loose the history. But we want to keep it. Let’s add this control before the save button:

We have an issue here. Assume, that on 4-th of June we have temporary lesson movement from the first cabinet to the second. Then we get the next requirement: since 28 of May all lessons start on 20:00 instead of 19:00. We open a form, change start time, set the affected range since 28.05 forever and.. all fields, including the cabinet number, pushed to the server and updated. The temporary 4-th June cabinet movement disappears. Based on this form, it’s impossible to predict what fields the user assumes to change, because it pushes all fields at once.

The idea is to split the basic form to several per-field forms and send each field independently. Every field represented by the set of rules. Each rule has type (the cabinet number, day of week, time start, duration, etc), scalar value, start and end date. It’s a weekly schedule, that’s why it’s enough to specify dates till the week number only in YYYYWW format.

Probably it seems that the slot editing is too complex now — if we want to change several fields, we have to submit a form several times. But practice says that changing several fields at once is a rare case. Much more often we have to bulk-update one property for several slots. For example, if the teacher has medical leave today, we select all his slots and set a new status at once.
The affected range is this day only. Then we assign a new teacher to the same selected blocks. Done. That’s how it works in starbright.com school management platform I’m currently working on:

Stripe coloring task

Imagine the stripe that is the row of colored cells one after another. Each cell represents a week. We have a new color and an interval we have to apply it for. We have to paint the new color over the stripe. On a data layer that means we have to cut the ends of partially overlapped intervals, remove overlapped,
insert a new interval and merge two nested intervals with the same value. The resulting intervals must not overlap.

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

That’s rather simple task, but it’s easy to miss something. It’s a good idea to complete it using TDD approach. I’ve created the separate repo with the solution and covered it with tests, http://timeblock-rules.rag.lt — here you can play with it.

Backend

Since the rules do not overlap, it’s enough to perform simple `select * from rules where from<=:week and (to is null or to>=:week)` to get the working rules for the specified week. https://github.com/Kasheftin/calendar-demo-backend — here you can find a very simple backend example on node/sequelize. It uses promises and async/await combined approach, you can check my other article for more details.

That’s the GET action 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 that’s the PATCH action for changing some set of rules:

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

That’s the most complex part of an application. Everything else is even more simpler.

One may ask how to remove a slot. Here we keep a full history, that’s why we don’t actually remove anything. But there’s status field that can be
opened, closed or temporary closed. Visitors can see only opened and temporary closed slots. Usually, when admin sets a slot as temporary closed, he adds a memo about why the slot got this status.

Frontend

You can find the code in this repo, it’s simple one page nuxt website. Actually, ssr has several difficulties (for example, here I wrote about how to implement authentication with nuxt), but it’s easy and fast to prototype simple apps.

That’s the code of the only page we have:

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

Fetch method works on server and client. It makes redirect to the current week and requests for the calendar data. The week watcher reloads the data.

The question is how to work with overlapping slots. Some applications solve it on the backend business logic, for example they deny overlapping. For the current application overlapping is allowed. Overlapped slots go one after another and have 1/2 of the initial slot width.

Then we add some styles and get the following picture:

Everything else is usual javascript without anything markable. Mousedown triggers the drag start event. Mousemove and mouseup events attached to the entire window. Drag event has 200ms delay to distinguish clicks and drags. Target droppable containers processed beforehand, because getBoundingClientRect is too heavy operation to work in mousemove on the fly. The application has two forms, one for creating and the other one for editing.

http://calendar.rag.lt — here you can check how it all works.

Links in this article:

✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.

--

--