Files
crr-shifts/docs/DOMAIN.md
T

16 KiB
Raw Blame History

Domain model

Schedules vs assignments

A schedule describes when a duty exists and how its initial assignee is chosen. An assignment is the actual duty on a concrete date. Once an assignment exists, it is not recalculated from the schedule.

This distinction is intentional: editing a rotation must not retroactively change an already published month.

Seeded schedules

Seeded names are Portuguese (Noite, Almoço - Sábados, Almoço - Domingo); the slugs stay night, saturday-lunch and sunday-lunch. Each schedule carries start_time/end_time, the hours the shift runs, editable by an administrator on its configuration page: 20:30–00:00 for the night and 13:00–16:00 for both lunches. An end at or before the start means the shift runs into the next day.

Backups

Every start copies the SQLite file aside before anything else runs, using SQLite's own backup API so the copy is consistent. Files go to backups/ next to the database (or BACKUP_DIR), are named rota-YYYYmmdd-HHMMSS.db and are never overwritten: two starts in the same second increment the name. Nothing is copied for another engine or an in-memory database, and a failed backup only logs a warning instead of stopping the app. Nothing is ever pruned, so the folder grows one file per start.

Calendar feeds

  • /calendar/<token>.ics is an iCalendar feed anybody can subscribe to from Google Calendar, Apple Calendar or Outlook. Every person has their own token, created the first time they open /calendar, and their feed only carries the shifts assigned to them.
  • /calendar is the page holding those links: the rota only has a button pointing there, so it keeps showing nothing but who works each day. A shared login gets one feed per person it signs in for, each titled Turnos de <pessoa>. Each address sits in a readonly field with a Copiar button (clipboard when the browser allows it, selecting the field otherwise) plus a webcal:// shortcut.
  • Administrators also get one shared feed with the whole rota, using the team token in CALENDAR_TOKEN (generated on first use and shown on /admin/schedules).
  • A feed publishes one month back and six months forward. Times carry the bar's timezone (DTSTART;TZID=Europe/Lisbon:20261017T203000) and the feed declares it in X-WR-TIMEZONE plus a VTIMEZONE block with the European summer-time rules (WET/WEST), so every client shows a night at 20:30 and keeps the right offset across the year. The zone comes from TIMEZONE; an unknown zone falls back to Europe/Lisbon. Times and names are private member data: the token is the only protection, so the shared link goes only to the team.
  • Each event title is <pessoa> · <escala> · <horas> so a list of shifts reads at a glance; a shift without a person keeps <escala> · <horas>.
  • An unknown token returns 404, and a deactivated person's token stops working.

Night (Noite)

  • Exists every day (Monday through Sunday).
  • Type: fixed.
  • Configured once as a repeating monthly pattern: one person per calendar day of the month (1 to 31), configured on /admin/schedules/<night id>.
  • Generation fills a missing assignment with the person configured for that day of the month. Days 29 to 31 only apply to months that have them.
  • There is no per-month configuration for Night: the same pattern is used every month.
  • Existing assignments are never rewritten by a pattern change; they keep the person they already had.
  • An administrator can explicitly Aplicar padrão from a chosen month and year on /admin/schedules/<night id>. It rewrites every existing generated night from the first day of that month onwards, in that month and every later one, to match the current pattern. It is the explicit way to push a corrected pattern onto shifts that already have a person. Assignment IDs, notes and completed request history are kept, manual assignments and completed swaps are preserved, days with nobody in the pattern are left as they are, and no assignment is ever created. Open and pending change requests on a rewritten shift are cancelled and the new and previous people are notified.

Saturday Lunch (Almoço - Sábados)

  • Saturdays only.
  • Type: rotation.
  • Has its own ordered member list.
  • Continues after the previous assigned person's position in the rotation, including across month boundaries. The anchor date is the fallback when there is no usable previous assignee.

Sunday Lunch (Almoço - Domingo)

  • Sundays only.
  • Type: rotation.
  • Has its own ordered member list, independent of Saturday.

The data model supports a schedule matching multiple weekdays even though the seeded lunch schedules each use one day.

Rotation generation

For a rotation schedule, use the last assigned person on an earlier date in that same schedule as the reference. Choose the next active member after that person's position in the current rotation order, wrapping to the beginning. Unassigned dates are skipped when finding the reference. Manual assignments and completed swaps also supply the reference for later dates. Saturday and Sunday never use each other's assignees.

Inactive members are skipped as recipients, but their position can still supply the reference if they remain in the rotation. When no previous assignee exists, or that person has been removed from the rotation, fall back to the number of matching schedule days from the anchor date modulo the active member count. An empty active rotation leaves the shift unassigned.

Generation and explicit month/range application process dates chronologically, so each newly assigned date supplies the reference for the next date. Regenerating one assignment only recalculates that date; later existing assignments remain unchanged until an administrator explicitly applies the rotation.

Only missing assignments are generated. Existing rows are never rewritten.

An administrator can explicitly Apply rotation on a lunch schedule's month page. This recalculates that month's generated assignments using the current active rotation members and fills missing dates for that schedule. Manual assignments and completed swaps are preserved. Simply editing the rotation order still does not rewrite any assignment.

Every assignment on a lunch schedule page has a Regenerate button. An administrator can explicitly regenerate any assignment, including an automatic assignment, manual override, or completed swap. It immediately recalculates that date from the current rotation and its source becomes generated; if there are no active rotation members, it becomes unassigned. These actions preserve assignment IDs, notes, and completed request history. If ownership changes, open or pending-approval change requests for the assignment are cancelled, and assignment-change notifications are attempted after saving.

Limpar turnos empties the generated shifts of a lunch schedule from the first day of a chosen month and year onwards, in that month and every later one, so they read as unassigned. No shift is deleted: rows keep their IDs, notes, source and completed request history, and stay generated, so Aplicar rotação fills them again. Manual assignments and completed swaps are never emptied. Open and pending change requests on an emptied shift are cancelled and the previous person is notified. Use it to force a recalculation from a known point instead of relying on the shifts that already exist.

People, logins and teams

  • email and phone are optional.
  • A person is somebody who works shifts. A person may be rota-only, with no way to sign in.
  • Signing in belongs to a login account (login_accounts): a username, a password, a label and the administrator flag. A person points at one account through users.login_id, and a person without one is rota-only. A person belongs to at most one account.
  • An administrator registers each person with one of three choices on /admin/users: sem login, conta nova (a username and a password created for that person alone) or conta já existente (an existing account, shared). Editing a person starts on manter the current access, so nothing is lost by saving the form without choosing.
  • A username belongs to the login account and is unique across accounts, not across people: several people may sign in with the same username.
  • /admin/logins lists the accounts: rename them, change the password, grant or withdraw administration, deactivate them, create an empty one ready to be shared, and remove an account once nobody uses it.
  • Administration is a property of the account, so everyone signing in through an administrator account is an administrator. An account cannot be deactivated, demoted or removed while it is the last one that can sign in as an administrator, and nobody can remove or deactivate the account they are signed in with.
  • Rota-only people can be assigned by an administrator, appear in rotations, and can be named as the target of a change request, but cannot answer it; an administrator completes the transfer on the schedule's own configuration page.
  • Any password of at least four characters is accepted; there is no complexity rule.
  • The session stores the login account, never a person. Everything a member sees and does is scoped to the people sharing that account: their shifts are the "mine" ones on the rota, their change requests are the ones listed, their calendar page holds one feed per person, and they can open and answer requests for any of them. A request made by one of them is their own: nobody in the account can accept it, and a swap that names a teammate as the other side is refused.
  • When a shared account accepts a request, the rota still records one person: the one the request names, or the account's first active person when the request names nobody. Ownership rules are checked against that person.
  • A signed-in person reaches their account through the top bar, which opens /account: a single field for the new password, with no confirmation and no retyping of the current one, so anyone holding an open session can change it. The page lists the people the account signs in for.
  • A person cannot be deactivated or lose their access while they are the only person of the account you are signed in with, and cannot lose it altogether on the people form.
  • An administrator can remove a person permanently from /admin/users, after a confirmation page that lists the impact. A person who still holds any assignment, past ones included, cannot be removed: those shifts must first be handed over, so the record of who worked each date stays intact. SQLite cascades are not enforced by the application, so the removal clears the remaining references explicitly: night-pattern days lose their person, rotation positions are deleted, that person's own change requests are deleted and the ones that merely targeted or accepted by them lose those references, and notification logs keep their history without the person. - Nobody may remove their own account or the last remaining administrator. The people page only shows the remove button for the people who pass these checks; the endpoints enforce them again. Deactivating is the non-destructive alternative.

Access

  • / is the rota and is the landing page for everyone, signed in or not. Signed-out visitors see the month read-only: no "mine" highlighting, no change-request actions.
  • /admin/schedules is the hub of the schedules and the only place that lists them: each card shows its kind and hours, the next dates of a rotation, its rotation order or the night pattern, and links to its configuration page. Signed-out visitors have no navigation at all; the brand logo is the way back to the rota.
  • Change requests, /swaps, and every /admin page still require a signed-in, active account.
  • Internal links are built with settings.url() (exposed to templates as app_path) and redirects go through the same helper, so ROOT_PATH keeps every link working under a proxy sub-path.

Change request state machine

open → pending_approval → approved

Other terminal states: rejected, cancelled, reverted.

  • A swap is always one shift for one shift: it is created on /swaps/new (its own page, so the rota keeps showing only who works each day) and names both shifts. The person giving the first shift is the requester; the person owning the second one is the target. There are no open offers.
  • The two shifts must belong to the same schedule. The people of a schedule are the ones in its rotation, in its monthly pattern, or already holding one of its shifts.
  • When a swap is approved or accepted, both assignments change hands at once: the target takes the requester's shift and the requester takes the target's shift. Both become swap and both people are notified.
  • One person can work several days in a month, so each side of the exchange is chosen from a list of the month's future shifts, grouped by schedule: the requester picks "O turno que passo" (their own shifts) and "O turno que quero em troca" (someone else's shifts of that schedule). A member only sees their own shifts on the first list; an administrator sees all of them.
  • A shift can only be part of one open request at a time, on either side.
  • An administrator can open a request for someone else's shift. The request is still recorded against the person who owns the shift, so the ownership rule below keeps working; unassigned and past shifts cannot be requested.
  • /admin/assign is the day-by-day picker for administrators: pick a day, then put someone on one of its shifts with Atribuir. This is a direct assignment, never a change request. It sets the assignment source to manual, cancels any open request that involves that shift, and notifies both the new and the previous person. Sem atribuição clears it. The date must match one of the schedule's weekdays.
  • The requester cannot accept their own request. With a shared login, that covers every person on it.
  • Only the person the request names can accept it or decline it, and with a shared login that means the account the named person signs in through.
  • If the schedule requires manager approval, acceptance moves to pending_approval.
  • Nobody can decline their own request; the requester cancels it instead. An administrator may decline any open or pending_approval request.
  • An administrator can approve a pending_approval request (it goes to whoever accepted it) or an open one, choosing another person of the same schedule when there is no named partner. Approval and decline are available both in /swaps and on the rota page next to the shift.
  • Approval transfers the assignment and sets its source to swap.
  • Only an administrator can revert an approved two-sided swap: both shifts go back to the people who had them, their source becomes manual (the original source cannot be recovered) and a note records the revert. If either shift changed after the swap, the revert is refused and the day-by-day assignment must be used instead.
  • If either person no longer owns their own shift, the request must not be applied and is cancelled.
  • On a schedule's configuration page, an administrator can complete an open or pending_approval request on someone's behalf by choosing another person who works that same schedule. This is the only way a rota-only person gives up or receives a shift. The same ownership rule applies, other open requests for that assignment are cancelled, and the requester and the new person are both notified. Past dates are refused.

Notifications

Email is optional per user and per installation.

Events include:

  • assignment updated/removed by admin;
  • change requested;
  • change accepted;
  • manager approval required;
  • change approved/rejected;
  • shift reminder.

An email error is logged but must not undo a scheduling action.