npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dienzt/shift-planner

v3.0.0

Published

Embeddable Vue 3 shift planner component with pluggable persistence adapters

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-planner

Peer-Dependencies müssen im Ziel-Projekt vorhanden sein:

npm install vue pinia

Schnellstart

// 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):

  1. adapter prop
  2. adapterConfig prop → selectAdapter(adapterConfig)
  3. Beliebiges initial*-Prop gesetzt → createMemoryAdapter(seed)
  4. 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: loadAssignments und loadWishes werden mit einem DateRange aufgerufen 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        # Prettier

Teststruktur

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() Hilfsfunktionen

Mock-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:lib

Erzeugt 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.