@dienzt/shift-planner
v3.0.0
Published
Embeddable Vue 3 shift planner component with pluggable persistence adapters
Maintainers
Readme
@peterbernstein/shift-planner
Einbettbare Vue 3 Dienstplan-Komponente mit austauschbaren Persistence-Adaptern, Drag-and-Drop-Zuweisung und Infinite-Scroll.
Installation
npm install @peterbernstein/shift-plannerPeer-Dependencies müssen im Ziel-Projekt vorhanden sein:
npm install vue piniaSchnellstart
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')<!-- App.vue -->
<template>
<ShiftPlanner />
</template>
<script setup lang="ts">
import { ShiftPlanner } from '@peterbernstein/shift-planner'
import '@peterbernstein/shift-planner/style.css'
</script>Ohne weitere Konfiguration verwendet die Komponente localStorage und füllt sich beim ersten Start mit Beispiel-Schichten und -Mitarbeitern.
Props
| Prop | Typ | Beschreibung |
|------|-----|-------------|
| adapter | PersistenceAdapter | Fertig gebaute Adapter-Instanz — höchste Priorität |
| adapterConfig | AdapterOptions | Adapter-Optionen (wird von selectAdapter() gebaut) |
| initialShifts | Shift[] | Schichten als Startwert (In-Memory-Modus) |
| initialPersons | Person[] | Mitarbeiter als Startwert (In-Memory-Modus) |
| initialAssignments | AssignmentRow[] | Zuweisungen als Startwert (In-Memory-Modus) |
| initialWishes | WishRow[] | Wünsche als Startwert (In-Memory-Modus) |
| initialTraits | PersonTraits[] | Eigenschaften als Startwert (In-Memory-Modus) |
Adapter-Auflösungsreihenfolge (erste Übereinstimmung gewinnt):
adapterpropadapterConfigprop →selectAdapter(adapterConfig)- Beliebiges
initial*-Prop gesetzt →createMemoryAdapter(seed) - Kein Prop →
localAdapter(localStorage)
Emits
Nach jeder Mutation (nach dem Initialisieren) werden folgende Events gefeuert:
| Event | Payload | Beschreibung |
|-------|---------|-------------|
| change | { shifts, persons, assignments, wishes, traits } | Vollständiger Snapshot des aktuellen Zustands |
| update:shifts | Shift[] | Aktualisierte Schichten |
| update:persons | Person[] | Aktualisierte Mitarbeiter |
| update:assignments | AssignmentRow[] | Flache Zuweisungszeilen |
| update:wishes | WishRow[] | Aktualisierte Wünsche |
| update:traits | PersonTraits[] | Aktualisierte Eigenschaften |
Template-Ref API (defineExpose)
<template>
<ShiftPlanner ref="planner" />
<button @click="exportData">Export</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ShiftPlanner } from '@peterbernstein/shift-planner'
import type { AssignmentRow } from '@peterbernstein/shift-planner'
const planner = ref<InstanceType<typeof ShiftPlanner> | null>(null)
function exportData() {
const assignments: AssignmentRow[] = planner.value?.getAssignments() ?? []
console.log(assignments)
}
</script>Verfügbare Methoden:
| Methode | Rückgabetyp | Beschreibung |
|---------|-------------|-------------|
| getShifts() | Shift[] | Alle Schichten |
| getPersons() | Person[] | Alle Mitarbeiter |
| getAssignments() | AssignmentRow[] | Alle Zuweisungen (flach) |
| getWishes() | WishRow[] | Alle Wünsche |
| getTraits() | PersonTraits[] | Alle Mitarbeiter-Eigenschaften |
Adapter-Konfiguration
localStorage (Standard)
Kein Adapter nötig — wird automatisch verwendet:
<ShiftPlanner />In-Memory (Props-getrieben)
Daten werden über Props übergeben und Änderungen über Emits zurückgegeben:
<ShiftPlanner
:initial-shifts="shifts"
:initial-persons="persons"
:initial-assignments="assignments"
@update:shifts="shifts = $event"
@update:persons="persons = $event"
@update:assignments="assignments = $event"
/>Supabase
import { createClient } from '@supabase/supabase-js'
import { createSupabaseAdapter } from '@peterbernstein/shift-planner'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Auth muss vor der Komponentenmontage abgeschlossen sein
const adapter = createSupabaseAdapter(supabase)<ShiftPlanner :adapter="adapter" />Django REST Framework
import { createDRFAdapter } from '@peterbernstein/shift-planner'
const adapter = createDRFAdapter(
'https://api.example.com',
() => localStorage.getItem('auth_token')
)<ShiftPlanner :adapter="adapter" />Eigener Adapter
Jede Klasse/jedes Objekt, das PersistenceAdapter implementiert, kann verwendet werden:
import type {
PersistenceAdapter, Shift, Person, AssignmentRow,
WishRow, PersonTraits, DateRange,
} from '@peterbernstein/shift-planner'
const myAdapter: PersistenceAdapter = {
async loadShifts() { return [] },
async saveShift(shift: Shift) { /* persist */ },
async deleteShift(id: string) { /* persist */ },
async loadPersons() { return [] },
async savePerson(person: Person) { /* persist */ },
async deletePerson(id: string) { /* persist */ },
async loadTraits() { return [] },
async saveTrait(trait: PersonTraits) { /* persist */ },
async loadAssignments(range: DateRange) { return [] },
async upsertAssignment(row: AssignmentRow) { /* persist */ },
async deleteAssignment(id: string) { /* persist */ },
async loadWishes(range: DateRange) { return [] },
async upsertWish(row: WishRow) { /* persist */ },
async deleteWish(id: string) { /* persist */ },
}Windowed Loading:
loadAssignmentsundloadWisheswerden mit einemDateRangeaufgerufen und sollen nur Zeilen für diesen Zeitraum zurückgeben. Die Komponente lädt zunächst ±8 Wochen und erweitert das Fenster per Infinite-Scroll in 4-Wochen-Schritten.
Datentypen
interface Person {
id: string
name: string
color: string // CSS-Farbe, z.B. '#10b981'
}
interface Shift {
id: string
name: string
color: string
requiredSkills?: string[]
weekConfig: ShiftWeekConfig
}
interface ShiftDayConfig {
personsRequired: number
durationHours: number
breakMinutes: number
enabled: boolean
}
// 0 = Montag … 6 = Sonntag
type ShiftWeekConfig = Record<0|1|2|3|4|5|6, ShiftDayConfig>
interface AssignmentRow {
id: string
dayIso: string // 'YYYY-MM-DD'
shiftId: string
personId: string
}
interface WishRow {
id: string
personId: string
dayIso: string // 'YYYY-MM-DD'
shiftId?: string // undefined = ganztägiger Wunsch
type: 'preferred' | 'unavailable'
note?: string
}
interface PersonTraits {
personId: string
skills: string[]
traits: string[] // z.B. 'senior', 'partTime', 'nightShiftCapable'
maxShiftsPerWeek?: number
}makeDefaultWeekConfig() erstellt eine ShiftWeekConfig mit Standardwerten (1 Person, 8h, 30min Pause, alle Tage aktiv).
Entwicklung
npm run dev # Dev-Server starten
npm run build # App-Build (vue-tsc + vite)
npm run build:lib # Library-Build → dist/
npm run test # Alle Tests einmalig (TZ=UTC)
npm run test:watch # Tests im Watch-Modus
npm run lint # Oxlint + ESLint mit --fix
npm run format # PrettierTeststruktur
src/__tests__/
adapter.test.ts — createMemoryAdapter + localAdapter
planner-store.test.ts — usePlannerStore (CRUD, Undo/Redo, Windowing)
persons-store.test.ts — usePersonsStore (Wünsche, Traits, Eligibility)
date-helpers.test.ts — Reine Datumsfunktionen
progress.test.ts — useProgress Composable
setup.ts — withSetup() + relDate() HilfsfunktionenMock-Daten für Tests und lokales Dev-Seeding:
import { MOCK_PERSONS, MOCK_SHIFTS, MOCK_ASSIGNMENTS, seedLocalStorage } from './mock-data'
// In main.ts für lokale Entwicklung:
if (import.meta.env.DEV) seedLocalStorage()Library-Build
npm run build:libErzeugt in dist/:
| Datei | Beschreibung |
|-------|-------------|
| shift-planner.js | ESM-Bundle |
| shift-planner.cjs | CommonJS-Bundle |
| shift-planner.css | Alle Stile (Light + Dark Mode) |
| index.d.ts | TypeScript-Deklarationen |
vue, pinia, @vueuse/core und vue-draggable-plus sind als peerDependencies deklariert und werden nicht gebündelt.
Supabase-Schema
Die Datenbankstruktur für den Supabase-Adapter liegt in src/migration.sql.
Dark Mode
Die Komponente unterstützt Dark Mode über CSS-Custom-Properties. Füge .dark zum Wurzel-Element hinzu:
document.documentElement.classList.toggle('dark')Alle --p-* CSS-Variablen können überschrieben werden, um das Design anzupassen.
