A browser-based shift planner for small and mid-sized teams. The app works week by week, supports fixed rotation patterns, and lets planners maintain schedules directly in the browser.
- Weekly planning with calendar weeks, public holidays, and school holidays
- Rotation patterns with a start week, cycle length, and template weeks
- Excel template export and import for rotation planning
- Staff, shift, and rotation management
- Planner role for shift assignments without full admin access
- Admin area for users, master data, and settings
- Audit log for manual schedule changes
- Automated releases, changelog generation, and Docker image publishing via GitHub Actions
Requirements:
- Node.js 20+
- npm
npm install
npm run devThe app starts at http://localhost:3000.
Set a bootstrap password before the first start:
$env:SHIFTPLAN_ADMIN_PASSWORD = "SecurePassword1"
npm run devChange this password after the first login.
The setup creates the initial admin user. Predictable defaults such as admin/admin are not generated. If an existing database still contains an unchanged default admin, startup is blocked until SHIFTPLAN_ADMIN_PASSWORD is set and node setup.js is run again.
Non-secret backend settings live in config/backend.config.json. This includes holiday regions, school holiday regions, session duration, rate limits, validation limits, shift defaults, analytics retention, XLSX limits, and proxy-header trust.
Secrets intentionally stay in .env, for example SHIFTPLAN_ADMIN_PASSWORD or Microsoft Graph credentials. Use SHIFTPLAN_BACKEND_CONFIG_PATH to load a different backend config file.
Proxy headers such as x-forwarded-for are ignored by default so clients cannot spoof their IP address to bypass rate limits. Set auth.trustProxyHeaders to true only when the app runs behind a trusted reverse proxy that overwrites these headers.
Holiday example:
{
"holidays": {
"public": {
"subdivisionCodes": ["SN"]
},
"school": {
"defaultSubdivisionCodes": ["SN", "BB"]
}
}
}docker build -t schichtplaner .
docker run --rm -p 3000:3000 --env-file .env -e SHIFTPLAN_ADMIN_PASSWORD=SecurePassword1 -v ${PWD}/db:/app/db schichtplanerDocker Compose starts the published image using the included compose.yaml:
docker compose up -dThe local .env file is not copied into the image. Pass it at runtime with --env-file .env or with env_file in Docker Compose so imprint and runtime variables are available inside the container. Compose reads .env for YAML interpolation, but it only passes values to the container when env_file is configured.
The Compose file mounts ./config/backend.config.json to /app/config/backend.config.json so backend settings can be changed without rebuilding the image.
Release images are pushed to GHCR:
ghcr.io/eric-schubert/shiftplan:latest
ghcr.io/eric-schubert/shiftplan:<version>
Contact requests are stored locally and can be reviewed in the admin area. Optionally, the server can also send a Microsoft Graph notification to an Exchange Online mailbox.
Required .env values:
CONTACT_MAIL_PROVIDER=graph
CONTACT_MAIL_TO=ziel@example.com
CONTACT_MAIL_GRAPH_TENANT_ID=
CONTACT_MAIL_GRAPH_CLIENT_ID=
CONTACT_MAIL_GRAPH_CLIENT_SECRET=
CONTACT_MAIL_GRAPH_FROM=postfach@example.com
CONTACT_MAIL_SUBJECT_PREFIX=[Schichtplaner]
CONTACT_MAIL_SAVE_TO_SENT_ITEMS=falseCreate a Microsoft Entra app registration with the Microsoft Graph application permission Mail.Send, grant admin consent, and ideally restrict the app to the sending mailbox. Without these variables, the contact form still works and stores requests locally only.
| Role | Can View | Can Plan | Can Manage |
|---|---|---|---|
| Public | Weekly plan, shifts, rotation | No | No |
| Planner | Everything from the public view | Assign shifts, import/generate rotations | No |
| Admin | Everything | Everything | Users, staff, shifts, settings |
Planners and admins can maintain rotation patterns with an Excel file:
- Download the Excel template in settings.
- Read the instructions in the first sheet.
- Check the start year, start week, and cycle length.
- Fill or adjust the template weeks.
- Import the file again.
- Generate the shift plan from the new pattern.
Internal IDs are not exposed for editing in the template. The visible fields are designed so the file can be edited without technical knowledge.
npm run dev # Development server
npm run test:run # Run tests once
npm run build # Production build
npm run docs # Regenerate README sectionsImportant paths:
| Area | Path |
|---|---|
| Pages | pages/ |
| Components | components/ |
| Stores | stores/ |
| Server API | server/api/ |
| Services | server/services/ |
| Tests | tests/ |
| Release/README scripts | scripts/ |
Releases are created automatically from Conventional Commits. The currently visible and deploy-relevant prefixes are:
feat:
fix:
perf:
security:
Hidden maintenance commits such as docs:, test:, ci:, or chore: do not create a release or Docker image.
The version history is available in the app through the version indicator in the header. New versions show only the newest entry on first visit; the full history remains available separately.
The following sections are maintained by scripts/readme-generator.js. They are collapsed by default so the README stays readable first.
Show project structure
schichtplaner/
|-- components/
| |-- app/
| | |-- ChangelogBanner.vue
| | `-- InstallBanner.vue
| |-- planner/
| | |-- holiday/
| | | |-- HolidayBannerCard.vue
| | | |-- HolidayCompactList.vue
| | | `-- HolidayDetailPanels.vue
| | |-- page/
| | | |-- PlannerBulkGenerateDialog.vue
| | | |-- PlannerCurrentWeekSection.vue
| | | `-- PlannerWeekHero.vue
| | |-- shift/
| | | |-- ShiftAssignDialog.vue
| | | `-- ShiftAssigneeList.vue
| | |-- HolidayInfo.vue
| | |-- ShiftCard.vue
| | `-- WeekPreview.vue
| |-- prime/
| | |-- PrimeCheckbox.vue
| | |-- PrimeColumn.vue
| | |-- PrimeDataTable.vue
| | |-- PrimeInputNumber.vue
| | |-- PrimeInputText.vue
| | |-- PrimeSelect.vue
| | |-- PrimeTab.vue
| | |-- PrimeTabList.vue
| | |-- PrimeTabPanel.vue
| | |-- PrimeTabPanels.vue
| | |-- PrimeTabs.vue
| | |-- PrimeTag.vue
| | `-- PrimeTextarea.vue
| `-- settings/
| |-- analytics/
| | `-- VisitAnalytics.vue
| |-- audit/
| | `-- AuditLog.vue
| |-- auth/
| | |-- AdminLogin.vue
| | `-- ChangePasswordDialog.vue
| |-- contact/
| | `-- ContactMessages.vue
| |-- rotation/
| | |-- RotationAssignDialog.vue
| | |-- RotationConfigDialog.vue
| | |-- RotationManager.vue
| | |-- RotationPatternBoard.vue
| | |-- RotationPatternIntro.vue
| | |-- RotationPatternWeekCard.vue
| | |-- RotationStaffPool.vue
| | |-- RotationToolbar.vue
| | |-- RotationWizardDialog.vue
| | |-- YearCopy.vue
| | |-- YearCopyDialog.vue
| | `-- YearCopyStatusPanel.vue
| |-- shifts/
| | |-- ShiftDeleteDialog.vue
| | |-- ShiftFormDialog.vue
| | |-- ShiftManagementHeader.vue
| | |-- ShiftManagementList.vue
| | `-- ShiftManager.vue
| |-- staff/
| | |-- StaffDeleteDialog.vue
| | |-- StaffFormDialog.vue
| | |-- StaffManagementHeader.vue
| | |-- StaffManagementList.vue
| | `-- StaffManager.vue
| `-- users/
| |-- UserCreateDialog.vue
| |-- UserDeleteDialog.vue
| |-- UserManagementHeader.vue
| |-- UserManagementList.vue
| `-- UserManager.vue
|-- layouts/
| `-- default.vue
|-- pages/
| |-- datenschutz.vue
| |-- impressum.vue
| |-- index.vue
| `-- settings.vue
|-- scripts/
| |-- create-release-tag.js
| |-- docker-smoke-test.sh
| |-- generate-changelog.js
| |-- generate-readme.js
| |-- readme-generator.js
| |-- release-prefixes.json
| |-- release-rules.js
| `-- resolve-version.js
|-- server/
| |-- api/
| | |-- analytics/
| | | |-- index.get.ts
| | | `-- visit.post.ts
| | |-- audit/
| | | `-- index.get.ts
| | |-- auth/
| | | |-- users/
| | | | `-- [id].delete.ts
| | | |-- change-password.post.ts
| | | |-- login.post.ts
| | | |-- logout.post.ts
| | | |-- session.get.ts
| | | |-- users.get.ts
| | | `-- users.post.ts
| | |-- contact/
| | | |-- messages/
| | | | `-- [id].patch.ts
| | | `-- messages.get.ts
| | |-- holidays/
| | | |-- public.get.ts
| | | `-- school.get.ts
| | |-- rotation/
| | | |-- assign.post.ts
| | | |-- config.get.ts
| | | |-- config.patch.ts
| | | |-- excel-import.post.ts
| | | |-- excel-template.get.ts
| | | |-- index.get.ts
| | | `-- unassign.post.ts
| | |-- shift/
| | | |-- [id].delete.ts
| | | |-- [id].get.ts
| | | |-- [id].patch.ts
| | | |-- index.get.ts
| | | `-- index.post.ts
| | |-- shiftplan/
| | | |-- assign.post.ts
| | | |-- copy-year.post.ts
| | | |-- generate.post.ts
| | | |-- index.get.ts
| | | |-- unassign.post.ts
| | | `-- year-summary.get.ts
| | |-- staff/
| | | |-- [id].delete.ts
| | | |-- [id].get.ts
| | | |-- [id].patch.ts
| | | |-- index.get.ts
| | | `-- index.post.ts
| | `-- contact.post.ts
| |-- config/
| | |-- analytics-config.ts
| | |-- auth-config.ts
| | |-- backend-config.schema.ts
| | |-- backend-config.ts
| | |-- backend-config.types.ts
| | |-- contact-config.ts
| | |-- contact-mail-config.ts
| | |-- database-config.ts
| | |-- domain-config.ts
| | `-- holiday-config.ts
| |-- middleware/
| | `-- auth.ts
| |-- plugins/
| | `-- compression.ts
| |-- services/
| | |-- analytics.service.ts
| | |-- audit.service.ts
| | |-- contact-mail.service.ts
| | |-- contact.service.ts
| | |-- rotation-excel.service.ts
| | |-- rotation.service.ts
| | |-- shift.service.ts
| | |-- shiftplan.service.ts
| | `-- staff.service.ts
| `-- utils/
| |-- analytics.ts
| |-- auth.ts
| |-- database-migrations.js
| |-- database.ts
| |-- session.ts
| |-- validation.ts
| `-- xlsx.ts
|-- stores/
| |-- data/
| | |-- rotation.ts
| | |-- shared.ts
| | |-- shifts.ts
| | `-- staff.ts
| |-- app.store.ts
| |-- auth.store.ts
| `-- data.store.ts
|-- types/
| |-- analytics.ts
| |-- auth.ts
| |-- contact.ts
| |-- holiday.ts
| |-- rotation.ts
| |-- shift.ts
| |-- shiftplan.ts
| `-- staff.ts
|-- components.d.ts
|-- nuxt.config.ts
|-- package.json
|-- README.md
|-- setup.js
|-- tailwind.config.js
|-- tsconfig.json
`-- vitest.config.ts
Show components
No components found.
Show API endpoints
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/staff |
Public | No | - | - | List staff records |
POST |
/api/staff |
Admin | Yes | - | active, is_parttime, name |
Create or update staff data |
GET |
/api/staff/:id |
Public | No | - | - | Read one staff record |
PATCH |
/api/staff/:id |
Admin | Yes | - | active, is_parttime, name |
Update one staff record |
DELETE |
/api/staff/:id |
Admin | Yes | - | - | Delete one staff record |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/shift |
Public | No | - | - | List shift records |
POST |
/api/shift |
Admin | Yes | - | color, end_time, min_staff, name, sort_order, start_time |
Create or update shift data |
GET |
/api/shift/:id |
Public | No | - | - | Read one shift record |
PATCH |
/api/shift/:id |
Admin | Yes | - | active, color, end_time, min_staff, name, sort_order, start_time |
Update one shift record |
DELETE |
/api/shift/:id |
Admin | Yes | - | - | Delete one shift record |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/shiftplan |
Public | No | week, year |
- | List shiftplan records |
POST |
/api/shiftplan/assign |
Planner/Admin | Yes | - | shift_id, staff_id, week, year |
Assign staff to a weekly shift |
POST |
/api/shiftplan/copy-year |
Planner/Admin | Yes | - | overwrite, sourceYear, targetYear |
Copy shift plans between years |
POST |
/api/shiftplan/generate |
Planner/Admin | Yes | - | week, weeks, year |
Generate plans from the rotation pattern |
POST |
/api/shiftplan/unassign |
Planner/Admin | Yes | - | shift_id, staff_id, week, year |
Remove staff from a weekly shift |
GET |
/api/shiftplan/year-summary |
Public | No | year |
- | Read yearly planning coverage |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/rotation |
Public | No | - | - | List rotation records |
POST |
/api/rotation/assign |
Planner/Admin | Yes | - | pattern_week, shift_id, staff_id |
Create or update rotation data |
GET |
/api/rotation/config |
Public | No | - | - | List rotation records |
PATCH |
/api/rotation/config |
Planner/Admin | Yes | - | cycle_length, start_week, start_year |
Update one rotation record |
POST |
/api/rotation/excel-import |
Planner/Admin | Yes | - | - | Create or update rotation data |
GET |
/api/rotation/excel-template |
Planner/Admin | No | - | - | List rotation records |
POST |
/api/rotation/unassign |
Planner/Admin | Yes | - | pattern_week, shift_id, staff_id |
Create or update rotation data |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
POST |
/api/auth/change-password |
Authenticated | Yes | - | currentPassword, newPassword |
Change the current user's password |
POST |
/api/auth/login |
Public | No | - | password, username |
Create a session and CSRF token |
POST |
/api/auth/logout |
Authenticated | Yes | - | - | Clear the current session |
GET |
/api/auth/session |
Authenticated | No | - | - | Read the current session state |
GET |
/api/auth/users |
Admin | No | - | - | List application users |
POST |
/api/auth/users |
Admin | Yes | - | password, role, username |
Create an application user |
DELETE |
/api/auth/users/:id |
Admin | Yes | - | - | Delete an application user |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/audit |
Admin | No | limit, offset, week, year |
- | List audit log entries |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/holidays/public |
Public | No | week, year |
- | Read public holidays |
GET |
/api/holidays/school |
Public | No | states, week, year |
- | Read school holidays |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
GET |
/api/analytics |
Admin | No | days |
- | List analytics records |
POST |
/api/analytics/visit |
Authenticated | Yes | path |
path |
Create or update analytics data |
| Method | Endpoint | Access | CSRF | Query | Body | Description |
|---|---|---|---|---|---|---|
POST |
/api/contact |
Authenticated | Yes | - | company, message, name, replyTo, subject |
Create or update contact data |
GET |
/api/contact/messages |
Admin | No | limit, offset |
- | List contact records |
PATCH |
/api/contact/messages/:id |
Admin | Yes | - | - | Update one contact record |
Show access matrix
| Method | Endpoint | Public | Planner | Admin | CSRF |
|---|---|---|---|---|---|
GET |
/api/staff |
Yes | Yes | Yes | No |
POST |
/api/staff |
No | No | Yes | Yes |
GET |
/api/staff/:id |
Yes | Yes | Yes | No |
PATCH |
/api/staff/:id |
No | No | Yes | Yes |
DELETE |
/api/staff/:id |
No | No | Yes | Yes |
GET |
/api/shift |
Yes | Yes | Yes | No |
POST |
/api/shift |
No | No | Yes | Yes |
GET |
/api/shift/:id |
Yes | Yes | Yes | No |
PATCH |
/api/shift/:id |
No | No | Yes | Yes |
DELETE |
/api/shift/:id |
No | No | Yes | Yes |
GET |
/api/shiftplan |
Yes | Yes | Yes | No |
POST |
/api/shiftplan/assign |
No | Yes | Yes | Yes |
POST |
/api/shiftplan/copy-year |
No | Yes | Yes | Yes |
POST |
/api/shiftplan/generate |
No | Yes | Yes | Yes |
POST |
/api/shiftplan/unassign |
No | Yes | Yes | Yes |
GET |
/api/shiftplan/year-summary |
Yes | Yes | Yes | No |
GET |
/api/rotation |
Yes | Yes | Yes | No |
POST |
/api/rotation/assign |
No | Yes | Yes | Yes |
GET |
/api/rotation/config |
Yes | Yes | Yes | No |
PATCH |
/api/rotation/config |
No | Yes | Yes | Yes |
POST |
/api/rotation/excel-import |
No | Yes | Yes | Yes |
GET |
/api/rotation/excel-template |
No | Yes | Yes | No |
POST |
/api/rotation/unassign |
No | Yes | Yes | Yes |
POST |
/api/auth/change-password |
No | Yes | Yes | Yes |
POST |
/api/auth/login |
Yes | Yes | Yes | No |
POST |
/api/auth/logout |
No | Yes | Yes | Yes |
GET |
/api/auth/session |
No | Yes | Yes | No |
GET |
/api/auth/users |
No | No | Yes | No |
POST |
/api/auth/users |
No | No | Yes | Yes |
DELETE |
/api/auth/users/:id |
No | No | Yes | Yes |
GET |
/api/audit |
No | No | Yes | No |
GET |
/api/holidays/public |
Yes | Yes | Yes | No |
GET |
/api/holidays/school |
Yes | Yes | Yes | No |
GET |
/api/analytics |
No | No | Yes | No |
POST |
/api/analytics/visit |
No | Yes | Yes | Yes |
POST |
/api/contact |
No | Yes | Yes | Yes |
GET |
/api/contact/messages |
No | No | Yes | No |
PATCH |
/api/contact/messages/:id |
No | No | Yes | Yes |
Show CI, release, and Docker details
| Workflow | Runs On | Main Result |
|---|---|---|
| CI | Push: master, main, RBA; PR: master/main | Tests, build, typecheck, and Docker smoke test |
| Auto Version & Release | Push: main, master | Creates version tag and GitHub release for changelog-visible commits |
| Docker Build & Push | CI success + deploy prefix: master, main, RBA | Builds and pushes GHCR image with generated changelog |
| Update README | Successful CI push: master, main | Regenerates README sections and commits with [skip ci] |
Release and deploy prefix rules are defined in scripts/release-prefixes.json.
Visible in releases: feat:, fix:, perf:, security:
Hidden from releases: refactor:, style:, test:, chore:, ci:, docs:, build:, revert:
- CI validates tests, typecheck, and production build.
- Auto Version & Release creates a tag for visible commit prefixes.
- Docker waits for the release tag, generates the in-app changelog, and pushes the image.
- Hidden prefixes such as docs, chore, ci, and test do not create releases or Docker images.
- README automation updates generated documentation after trusted pushes without retriggering CI.
MIT License. See LICENSE for details.