Replies: 6 comments
|
Thanks for moving this over! I agree that #749 and this proposal are closely related. A fixed weekly timetable and a rotating shift schedule could probably share the same underlying schedule/pattern concept rather than being implemented as two completely independent features. My preference would be to keep the schedule/pattern data separate from normal calendar events and expose it as an optional calendar overlay. The calendar would visualize the resulting schedule, but wouldn’t necessarily need to own all of the shift/pattern-specific logic. Conceptually, I could imagine a more general Personal Schedule supporting different kinds of patterns:
Both fixed and rotating schedules could then optionally be shown in the existing calendar and toggled on/off there. Regarding recurrence, I think this is where shift patterns differ somewhat from normal calendar recurrence. A calendar rule works very well for things like “every Monday at 08:00”, but many shift systems are based on a continuously rotating cycle rather than weekdays. For example: Early → Early → Late → Late → Night → Night → Off → Off → repeat The next occurrence is determined by the position in the cycle, not by the day of the week. Ideally a user could define such a pattern once, choose a starting date, and have it continue from there. I would still keep context-specific functionality separate. Things such as worked-hours statistics, night/weekend/public-holiday allowances, etc. make sense for a work schedule but probably shouldn’t complicate a basic school timetable. So personally I would lean towards one underlying schedule/pattern system shared with #749, with optional calendar integration, rather than modelling every shift directly as an ordinary calendar event. That would also leave room for some of the optional ideas from #661 later, such as importing a shift plan from a PDF/screenshot or creating a pattern from natural-language input, without making any of that a requirement for the initial implementation. |
|
One additional thought regarding recurrence: I think this is probably one of the areas where the shift planner would need to go beyond the calendar’s existing recurrence rules. Real-world shift systems can vary quite a lot, so ideally the implementation shouldn’t have hard-coded concepts such as “4 on / 2 off”, “every second weekend”, etc. Instead, I think a generic cycle-based pattern model could cover most use cases. For example, a pattern could essentially consist of:
A simple 8-day rotation could then look like this: Or, more simply:
The important difference to normal calendar recurrence is that it is not necessarily the same event repeating. It is a sequence of different entries repeating as one cycle and that cycle may be completely independent of weekdays or calendar weeks. Recurrence / pattern capabilities I think the underlying model should ideally be flexible enough to support:
The last two points might be especially important from a data-model perspective. Pattern history / effective dates For example, somebody might work Pattern A from January to June and switch to Pattern B on July 1st. Changing the active pattern shouldn’t retroactively change their historical schedule. Conceptually, pattern assignments could therefore have something like: This would preserve historical schedules while still allowing future schedules to change. Overrides / exceptions Individual occurrences should probably also be overridable without modifying or breaking the underlying rotation. For example: Other examples could be: or simply: This could be useful for vacation, sickness, training, shift swaps, overtime or just one-off deviations from the normal rotation. Possible data model That might suggest separating the concepts into roughly four layers:
Reusable definitions such as: These could contain the name, color, icon and default start/end times.
Define the actual sequence:
Connect a pattern to a household member and a time period:
Modify individual occurrences without touching the underlying pattern: Calculating occurrences instead of generating events The calendar could then resolve occurrences for the requested date range rather than necessarily storing thousands of generated calendar events. For an 8-day cycle, the pattern position for a given date can conceptually be derived from:
For example: Overrides could then be applied on top of the resolved occurrence. That would avoid having to generate years of future calendar events just because somebody has an indefinite shift rotation. The calendar integration could effectively behave like an overlay/view of the schedule data, which could then be enabled or disabled independently. Relation to #749 I think #749 could potentially use exactly the same underlying pattern system. A fixed weekly timetable is essentially a 7-day pattern anchored to weekdays: A rotating shift schedule would simply be the more general case where the cycle length and anchor aren’t necessarily tied to a calendar week. A two-week timetable would similarly just be a 14-day pattern. So the underlying concept could potentially be shared without forcing the UI or functionality to be identical. Edge cases There are also some edge cases worth keeping in mind, even if they aren’t all part of an initial implementation:
Keeping the underlying occurrences structured would also be useful if the optional statistics/allowances from #661 are ever implemented later. For example, it could eventually allow calculations such as: and potentially calculate configurable allowances based on those hours. So overall, I would favor a fairly generic cycle + assignment + exception model rather than extending the calendar recurrence rules with lots of shift-specific recurrence types. The UI could still provide convenient presets such as:
but those could all map to the same generic pattern model underneath. That would keep the common cases easy to configure while still being flexible enough for the many different shift systems people actually work. |
|
@ulsklyc Are there any plans to add this feature? I could also work on it and create a PR. |
|
Yes - and thank you for the offer. Before you write any code, though, the decision that has been sitting on my side needs making, because leaving an architectural question with a contributor is a mistake I have made once in this repository and I am not repeating it. Three decisions, and they are mine rather than yours: 1. One model, not two. You and Benoit in #749 are describing the same thing. A "Week A / Week B" school timetable is a 14-day cycle anchored to a date; your Early-Early-Late-Late-Night-Night-Off-Off is an 8-day one. Building the fixed weekly timetable as its own feature would mean implementing the same arithmetic a second time six months later. Your generic cycle model covers both, and that is what goes in. 2. A pattern, not calendar recurrence. Worth being precise about why, because the calendar could technically do it: six 3. An overlay, not materialised events. Your preference matches mine, with a second argument for it: a rotation running two years is roughly 700 rows per person if every day becomes an event, and each pattern edit then has to reconcile them. Computed from anchor date plus cycle position it is one row for the pattern plus one per override. The calendar renders it and does not own it. Three things I would still like your view on, since you are the one living with this:
If that shape works for you, the PR is welcome. I will write the data model up here first - patterns, shift types and overrides - so you are not guessing at table layouts, and it lands as a module that ships switched off, the way Inventory does. Say so and I will post the spec next. |
|
As promised, the data model. Long post, but it is the thing that lets you start without guessing, and everything below is a decision rather than an option. Name and scopeThe module is Four tables, three conceptsI said three in my last comment. It is four, because the cycle is an ordered list and that is a table of its own rather than a JSON column - the cycle needs to be queried, and a JSON blob is the kind of second format that stops being readable the moment something has to reconcile it. All of this goes in as migration 160 (159 is the highest today). Migrations are append-only in -- A reusable shift type. Household-wide, because two people on the same ward
-- share the same "Early". Times are wall-clock strings, never instants.
CREATE TABLE IF NOT EXISTS schedule_shift_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
short_code TEXT, -- "E", "N" - what fits in a calendar cell
start_time TEXT, -- 'HH:MM', NULL for all-day (vacation, training)
end_time TEXT, -- 'HH:MM'; < start_time means it ends next day
color TEXT NOT NULL DEFAULT '#6C3AED',
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
CHECK ((start_time IS NULL) = (end_time IS NULL))
);
-- One pattern per person per purpose. "My rotation", "School timetable".
CREATE TABLE IF NOT EXISTS schedule_patterns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
anchor_date TEXT NOT NULL, -- 'YYYY-MM-DD', day 1 of the cycle
cycle_length INTEGER NOT NULL CHECK (cycle_length BETWEEN 1 AND 366),
valid_from TEXT, -- NULL = open at that end
valid_until TEXT,
is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
-- Position within the cycle. A missing row, or a NULL shift type, is a free day.
CREATE TABLE IF NOT EXISTS schedule_pattern_days (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern_id INTEGER NOT NULL REFERENCES schedule_patterns(id) ON DELETE CASCADE,
position INTEGER NOT NULL CHECK (position >= 0), -- 0-based, < cycle_length
shift_type_id INTEGER REFERENCES schedule_shift_types(id) ON DELETE RESTRICT,
UNIQUE (pattern_id, position)
);
-- One calendar day that deviates. The row's existence IS the override.
CREATE TABLE IF NOT EXISTS schedule_overrides (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
date_key TEXT NOT NULL, -- 'YYYY-MM-DD'
shift_type_id INTEGER REFERENCES schedule_shift_types(id) ON DELETE RESTRICT,
note TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
UNIQUE (user_id, date_key)
);
CREATE INDEX IF NOT EXISTS idx_schedule_patterns_user ON schedule_patterns(user_id);
CREATE INDEX IF NOT EXISTS idx_schedule_overrides_user ON schedule_overrides(user_id, date_key);Plus the The decisions inside that DDLA free day is An override is its row. No Vacation and training are shift types, not a fourth concept. They are all-day (
The arithmetic, and the trap in itPosition in the cycle for a given day: const days = daysBetween(pattern.anchor_date, dateKey);
const position = ((days % pattern.cycle_length) + pattern.cycle_length) % pattern.cycle_length;The double modulo is not decoration. Days before the anchor date are negative, and JavaScript's
Two more rules from this codebase that a new module gets wrong by default:
Resolution orderFor one user and one day, in this order, first hit wins:
If two active patterns cover the same day, the one with the later Surface
To your second question - what the household sees - my instinct was that a quieter marker is enough, but you live with it and I would rather have the model carry both: the entry returns the shift type with its times, and whether the calendar draws a full block or a strip is a display setting, not a data one. The Module registration
Tests
Where to stopFirst version: fixed weekly patterns, rotating cycles, per-day overrides, calendar overlay. Out: shift swapping between members as a workflow, time-off requests and approvals, anything with staffing levels. If v1 lands well, "who else is working tomorrow" is the obvious second step, and it is much easier to add on top of this than to carve out of a bigger first attempt. Fire away with questions before you write code rather than after - and if any of the above reads wrong against how your rota actually works, say so now. I would rather change the model on this page than in a migration. |
|
Released in v2.48.0. Schedule is in, contributed by @mclgoerg - the cycle model above is what shipped, unchanged in its architecture. What landed, against the decisions in this thread:
Closing this as resolved. #749 stays open on its own: the machinery is now there, and what is left for it is the timetable's own vocabulary rather than the cycle arithmetic. |
Uh oh!
There was an error while loading. Please reload this page.
Moved here from issue #661 so that feature ideas live where the rest of the backlog is discussed. Originally proposed by @mclgoerg - the full proposal, including the detailed data model and UI sketch, is in #661.
The idea. A Personal Shift Planner module where household members manage their own recurring work schedules, optionally shown in the calendar. Explicitly not workforce scheduling: the goal is helping a family coordinate everyday life around personal shifts.
Why. Many households include people working rotating or irregular shifts - healthcare, emergency services, retail, hospitality, manufacturing, logistics. Knowing who is working when makes family activities, appointments, childcare and household tasks much easier to plan.
Related but not the same: #749 (Weekly Timetable) covers a fixed weekly schedule rather than rotating shift patterns. Worth deciding whether these are one module or two.
Open questions from my side: whether shifts should be their own module or a shift-pattern layer on top of the calendar, and how far the recurrence needs to go beyond what the calendar's recurrence rules already do.
All reactions