diff --git a/.agents/skills/work-in-nextcloud-app/SKILL.md b/.agents/skills/work-in-nextcloud-app/SKILL.md
index 35e0ff6..4650c39 100644
--- a/.agents/skills/work-in-nextcloud-app/SKILL.md
+++ b/.agents/skills/work-in-nextcloud-app/SKILL.md
@@ -57,7 +57,9 @@ Stop immediately if production systems, Git history rewriting, new production de
- Develop executable UI logic test-first; additionally cover layout, accessibility, and Nextcloud integration with suitable smoke or browser checks.
- Treat a time-boxed exploratory spike or hard-to-isolate Nextcloud integration as an explicitly justified deviation. Discard spike code or characterize it before adoption; choose the truthful broader integration level when isolation would hide the real contract.
- Use the local fast entries named by `AGENTS.md`, normally `php tests/run.php` and `node tests/run-js.mjs`; dependency-light PHP smokes run in isolated processes. Run LocalBase and every affected consumer contract/smoke suite after a LocalBase contract change.
-- In local pre-production, app tests may use shared LocalBase test helpers through relative repository paths. Add heavier packaging/autoload structure or a larger test framework only when path handling, runners, assertions, mocks, or fixtures are materially duplicated or impair readability.
+- PHP classes must not remain coupled through distributed relative `require` or `require_once` chains. Production classes under `lib/` use Nextcloud's PSR-4 app autoloader; a bundled category-A dependency uses exactly one reproducibly generated, app-local, namespace-isolated Composer autoloader. Never use a shared workspace autoloader, shared cross-app `vendor/`, or neighboring production repository path as a delivery mechanism. Category-B services remain separate runtime apps and are consumed only through their public activation- and version-aware contracts.
+- Every app uses one central app-local test bootstrap/autoloader for dependency-light PHP tests. It may resolve a test-only LocalBase helper through exactly one central transition point until a versioned development dependency exists, but individual tests must not retain direct relative LocalBase class paths after migration. Nextcloud integration tests may load the documented Nextcloud test bootstrap; template partials, explicit process/test entrypoints, and the one-time bootstrap of a bundled Composer autoloader remain justified includes rather than class-load chains.
+- When a writing task first touches executable PHP or PHP tests in an app that has not yet migrated, include that app's complete autoload migration as a separate preparatory step in the same app scope. An app is complete only when distributed manual class requires and production fallback requires are gone, allowed includes are limited and reviewable, relevant app and provider/consumer PHP tests are green, and release checks contain neither development dependencies nor foreign repository paths. Documentation-only, formatting-only, and JavaScript-only work does not trigger an artificial PHP migration.
- Known overall and app coverage must not decline unnoticed. Aim for at least 85 percent line coverage for new or materially changed executable code, report PHP and JavaScript separately, and fully cover security invariants regardless of percentages. Coverage is a warning and delivery indicator, not a substitute for meaningful assertions.
- Do not prepare a commit or release with red relevant fast tests, contract tests, security checks, coverage gates, or delivery gates.
@@ -82,6 +84,7 @@ Stop immediately if production systems, Git history rewriting, new production de
## Git and completion report
+- When Simon asks for the next open steps, priorities, remaining work, or a similar outlook, include every applicable pending migration and unresolved decision from accepted ADRs and documented rollout plans. Report its current status, trigger, and required approval gate, and distinguish work executable now from work triggered by a later app change and work that is currently undecidable. Mentioning an item does not expand the current write scope or authorize a gated change.
- Do not commit, push, release, deploy, or use `git add .` without Simon's explicit authorization. Stage individual files only when staging was requested.
- Before a commit, show `git status --short`, `git diff --stat`, and `git diff --name-only`. Never use `git reset --hard`, `git clean`, force-push, history rewrite, or versioned backup copies.
- Run relevant local tests, `git diff --check`, and the repository's own structure/fast check. For an explicitly authorized cross-app contract change, validate every provider and consumer repository from its own root and use the Parent workspace check only as an additional coordinator.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 76644ec..49e9b3f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -106,3 +106,12 @@ jobs:
--check-coverage \
--lines=86.47 \
node tests/run-js.mjs
+
+ deploy-staging:
+ name: Staging-Deployment
+ if: github.event_name == 'push'
+ needs: [php, javascript]
+ uses: Filzmann/br-nextcloud-apps/.github/workflows/deploy-staging.yml@main
+ with:
+ app-id: adplaner
+ secrets: inherit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de4616b..a5cdcf1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Changelog
+## 0.4.0-rc.2
+
+- Monatsplanänderungen und Statuswechsel gegen konkurrierende Freigaben serialisiert; genehmigte Schichtdefinitionen bleiben als unveränderlicher Planstand erhalten.
+- Strikte Monats-, Datums-, UTF-8-, Längen-, Boolean- und Schichtlistenvalidierung ergänzt.
+- Teameinstellungen, Tagesbemerkungen und Zuweisungen beim konkurrierenden erstmaligen Speichern gegen Unique-Constraint-Konflikte abgesichert.
+- Geleerte Tagesbemerkungen samt Bearbeitungsmetadaten entfernt; öffentliche Team- und Hinweisantworten auf aktuell schichtfähige Personen und benötigte Anzeigenamen reduziert.
+- Technische Monats-Locks von personenbezogenen Änderungsmetadaten getrennt und interne Demo-Fehler ohne Detailleck als Serverfehler beantwortet.
+- Fehler optionaler Hinweisprovider isoliert und öffentliche Planpayloads um interne Erstellerkennungen sowie teamfremde Hinweise bereinigt.
+- Lade-, Speicher- und Zuteilungsoberfläche gegen veraltete Antworten, Doppelstarts, unklare Nachladefehler sowie Tastatur-, Tabellen- und Fokusprobleme stabilisiert.
+
+## 0.4.0-rc.1
+
+- Kontrollierte Monatsplanstatus `draft`, `planned` und `approved` ergänzt; nur die zuständige Einsatzbegleitung darf wechseln und genehmigte Pläne sind gegen Änderungen gesperrt.
+- Optionale, datensparsame Urlaubs- und Kalenderhinweise über die öffentlichen LocalBase-Verträge eingebunden; fehlende Provider bleiben ein gültiger Standalone-Zustand.
+- Direkte Navigation zum vorherigen und nächsten Monat sowie sichtbarer Tabellen-Scrollbereich und fixierte Bemerkungsspalte ergänzt.
+- Additive Monatsplanstatus-Migration sowie automatisierte Service-, UI-, Migrations-, Datenbank- und DDEV-Rechteprüfungen ergänzt.
+
## 0.3.0-rc.1
- Eigenständige Navigation ohne OrgSuite ergänzt.
diff --git a/README.md b/README.md
index 1dc53d5..db4c08b 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,16 @@ AdPlaner funktioniert einzeln; optionale Abwesenheits- oder Kalenderhinweise ent
Assistenzteams werden aus den zentral konfigurierten Nextcloud-Gruppen abgeleitet. Teambezogene Schichtkonfigurationen werden durch berechtigte Einsatzbegleitungen gepflegt.
+Die zuständige Einsatzbegleitung führt Monatspläne kontrolliert von `draft` über `planned` nach `approved`. Genehmigte Pläne sind bis zu einer ausdrücklichen Rücknahme gegen Wünsche, Zuweisungen und Bemerkungsänderungen gesperrt. Optionale Urlaubs- und Kalenderprovider liefern ausschließlich datensparsame, schreibgeschützte Planungshinweise.
+
+Genehmigte Pläne frieren die damaligen Schichtdefinitionen ein, speichern aber keine zusätzlichen historischen Personenstammdaten. Zuweisungen werden weiterhin nur für aktuell schichtfähige Mitglieder des jeweiligen Assistenzteams angezeigt.
+
## Roadmap
Geplante Erweiterungen und offene Produktentscheidungen stehen in der [Roadmap](ROADMAP.md).
+Für die fachliche, visuelle und sicherheitsbezogene Staging-Prüfung steht ein
+ausfüllbares [manuelles Abnahmeformular](docs/manual-acceptance.md) bereit.
+Zugangsdaten und personenbezogene Echtdaten werden darin nicht dokumentiert.
+
Installations-, Betriebs- und Abnahmeunterlagen stehen im öffentlichen [AD-Suite-Projekt](https://github.com/Filzmann/ad-suite).
diff --git a/ROADMAP.md b/ROADMAP.md
index ee24f56..4e50044 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -4,18 +4,29 @@ Diese Datei bündelt geplante Erweiterungen und offene Produktentscheidungen. Ve
## Aktueller Fokus
+- Die manuellen Prüfungen werden im ausfüllbaren
+ [`docs/manual-acceptance.md`](docs/manual-acceptance.md) dokumentiert.
- Produktive Rechte- und Datenschutzprüfung der Wunschdienstplanung.
- Monatsplan, variable Schichten, EB-Koordination und Standalone-Betrieb auf einem realitätsnahen Staging fachlich abnehmen.
## Geplante Erweiterungen
+- **ADP-L10N – vollständige Lokalisierung (später, nicht freigegeben):**
+ AdPlaner wird im Rahmen des suiteweiten L10N-Rollouts auf die aktive
+ Nextcloud-Locale und Nextcloud-l10n umgestellt. Manuelle Monats- und
+ Wochentagsnamen sowie sichtbare UI-, Status-, Validierungs- und
+ Fehlermeldungen werden dabei vollständig migriert. ISO-Daten,
+ Monatsnummern, Schichtzeiten, Statuswerte, Teamcodes und API-Schlüssel
+ bleiben unverändert; Abkürzungen werden nicht durch Abschneiden gebildet.
+ Erforderlich sind Tests für deutsche Ausgabe, mindestens eine weitere
+ Locale, Fallback, Monats-/Jahresgrenzen, Pluralformen, Platzhalter und
+ Escaping in PHP und JavaScript. Pilot-App, Reihenfolge und Rohtext-Gate
+ werden vor Umsetzung suiteweit separat freigegeben.
- Persönliche Monatsansicht „Alle meine Einsätze“ mit PDF-Export und optionaler Verbindung zu gängigen Kalendern.
- Benachrichtigungen für relevante Planungs- und Statusänderungen.
-- Fachlich eindeutige Festschreibung eines Dienstplans.
- Teambezogene Konfigurierbarkeit nur dort erweitern, wo konkrete Teams unterschiedliche Regeln benötigen.
## Vor der Umsetzung zu klären
- Exportformate, Zielsysteme und Datenschutzumfang.
- Benachrichtigungskanäle, Empfänger*innen und auslösende Ereignisse.
-- Bedeutung, Rechte und Rückbau einer Festschreibung sowie der Umgang mit späteren Änderungen.
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 8ba1330..3216955 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -5,14 +5,14 @@
Assistenz DienstplanungWunschdienstplanung für Assistenzteams.Verwaltet monatliche Wunschdienstpläne für Assistenzteams auf Basis dynamischer Nextcloud-Gruppen.
- 0.3.0-rc.2
+ 0.4.0-rc.2agplSimon
+ AdPlaner
+ organizationhttps://github.com/Filzmann/ad-suitehttps://github.com/Filzmann/nextcloud-adplaner/issueshttps://github.com/Filzmann/nextcloud-adplaner
- AdPlaner
- organization
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 0506afe..4efa5be 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -6,10 +6,11 @@
['name' => 'api#state', 'url' => '/api/state', 'verb' => 'GET'],
['name' => 'api#monthPlan', 'url' => '/api/teams/{teamCode}/months/{month}', 'verb' => 'GET'],
+ ['name' => 'api#transitionMonthStatus', 'url' => '/api/teams/{teamCode}/months/{month}/status', 'verb' => 'POST'],
['name' => 'api#saveTeamSettings', 'url' => '/api/teams/{teamCode}/settings', 'verb' => 'POST'],
['name' => 'api#saveDayNote', 'url' => '/api/teams/{teamCode}/months/{month}/days/{workDate}/note', 'verb' => 'POST'],
- ['name' => 'api#addShiftCandidate', 'url' => '/api/teams/{teamCode}/months/{month}/slots/{slotId}/candidates', 'verb' => 'POST'],
- ['name' => 'api#removeShiftCandidate', 'url' => '/api/teams/{teamCode}/months/{month}/slots/{slotId}/candidates/remove', 'verb' => 'POST'],
+ ['name' => 'api#addShiftCandidate', 'url' => '/api/teams/{teamCode}/months/{month}/slots/{slotId}/candidates', 'verb' => 'POST', 'requirements' => ['slotId' => '\\d+']],
+ ['name' => 'api#removeShiftCandidate', 'url' => '/api/teams/{teamCode}/months/{month}/slots/{slotId}/candidates/remove', 'verb' => 'POST', 'requirements' => ['slotId' => '\\d+']],
['name' => 'demo_admin#install', 'url' => '/api/admin/demo-pack/install', 'verb' => 'POST'],
],
];
diff --git a/css/style.css b/css/style.css
index 3e5ff86..8175b4b 100644
--- a/css/style.css
+++ b/css/style.css
@@ -26,6 +26,8 @@
.adp-controls,
.adp-tabs,
.adp-section-head,
+.adp-plan-meta,
+.adp-plan-status,
.adp-cell-actions {
display: flex;
align-items: center;
@@ -101,6 +103,7 @@
.adp-table-wrap {
overflow: auto;
max-width: 100%;
+ max-height: calc(100vh - 260px);
}
.adp-table {
@@ -133,6 +136,18 @@
border-left: 1px solid var(--color-border);
}
+.adp-month-table th:last-child,
+.adp-month-table td:last-child {
+ position: sticky;
+ right: 0;
+ z-index: 2;
+ box-shadow: -1px 0 0 var(--color-border);
+}
+
+.adp-month-table thead th:last-child {
+ z-index: 4;
+}
+
.adp-table small,
.adp-day small {
display: block;
@@ -156,6 +171,25 @@
color: var(--color-main-text);
}
+.adp-day-hints {
+ display: grid;
+ gap: 3px;
+ margin-top: 6px;
+}
+
+.adp-day .adp-hint {
+ display: inline-flex;
+ width: max-content;
+ max-width: 160px;
+ border-radius: 6px;
+ padding: 1px 5px;
+ background: var(--color-background-hover);
+ color: var(--color-main-text);
+ font-size: .8rem;
+ font-weight: 600;
+ white-space: normal;
+}
+
.adp-candidates {
display: flex;
align-items: flex-start;
@@ -199,15 +233,48 @@
text-align: center;
}
-.adp-assignment-control,
-.adp-assignment-picker {
+.adp-month-control {
display: inline-flex;
align-items: center;
gap: 4px;
}
-.adp-assignment-picker select {
- width: 150px;
+.adp-month-control .adp-icon-button {
+ min-height: 34px;
+ margin: 0;
+}
+
+.adp-assignment-control {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.adp-assignment-picker {
+ position: absolute;
+ left: 0;
+ top: calc(100% + 4px);
+ z-index: 5;
+ display: grid;
+ gap: 4px;
+ min-width: 180px;
+ max-height: 240px;
+ overflow-y: auto;
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ padding: 6px;
+ background: var(--color-main-background);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
+}
+
+.adp-assignment-picker[hidden] {
+ display: none;
+}
+
+.adp-assignment-picker .adp-small {
+ width: 100%;
+ text-align: left;
}
.adp-note-cell textarea {
diff --git a/docs/manual-acceptance.md b/docs/manual-acceptance.md
new file mode 100644
index 0000000..5db11ee
--- /dev/null
+++ b/docs/manual-acceptance.md
@@ -0,0 +1,181 @@
+# Manuelles Abnahmeformular – AdPlaner
+
+Dieses Formular dokumentiert die fachliche und visuelle Abnahme der
+Assistenzplanung auf einem realitätsnahen Staging-System. Pro Prüffall wird
+genau ein Ergebnis markiert und unter „Warum/Beleg/Abweichung“ knapp
+festgehalten, was beobachtet wurde.
+
+Keine personenbezogenen Echtdaten, Gesundheits- oder Urlaubsdetails,
+Zugangsdaten oder internen Kennungen eintragen. Ausschließlich neutrale
+Testkonten, synthetische Teams, Schichten und Bemerkungen verwenden.
+
+## Kopfdaten
+
+| Feld | Eintrag |
+|---|---|
+| Datum und Uhrzeit | 02.08.2026, ca. 18:26–19:31 Uhr |
+| Prüfer*in | Simon |
+| Umgebung und URL | DEV/Staging – `https://nextcloud-dev.ddev.site/index.php/apps/adplaner/` |
+| AdPlaner-Version | 0.3.0-rc.2 |
+| Nextcloud-Version | Nextcloud Hub 26 Spring – 34.0.2 |
+| Browser und Version | Google Chrome 150.0.7871.186, offizieller 64-Bit-Build unter Ubuntu |
+| Fenstergröße / Zoom | ungefähr zwei Drittel von 1920 px, Zoom 100 % |
+| Neutrale Testkonten, Teams und Rollen | `adc-test-eb-sued` (EB-Testkonto), `admin` (in AdPlaner normales schichtfähiges Teammitglied ohne EB-Rechte), `west` (Nur-EB-Testkontext); Teams `KaKü` und `HaHü`; weitere neutrale Testkonten im Verlauf verwendet, aber nicht einzeln dokumentiert |
+| Aktive optionale Apps | Grundsätzlich alle, einschließlich OrgSuite, AD Urlaub und AD Kalender; OrgSuite für A1 vorübergehend deaktiviert, AD Urlaub für D1 und AD Kalender für D2 vorübergehend deaktiviert |
+
+Ergebniskennzeichnung: `[x] erfolgreich` / `[x] nicht erfolgreich` /
+`[x] nicht geprüft`. Bei „nicht erfolgreich“ oder „nicht geprüft“ ist eine
+Begründung verpflichtend.
+
+## A. Einstieg, Navigation und Monatsplan
+
+| ID | Was wird geprüft? | Auszuführende Schritte | Erwartetes Ergebnis | Ergebnis | Warum/Beleg/Abweichung |
+|---|---|---|---|---|---|
+| A1 | Standalone-Einstieg | AdPlaner ohne aktive OrgSuite öffnen. | Ein eigener Nextcloud-Einstieg ist vorhanden und der Monatsplan wird ohne andere Fachapps geladen. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Eigener Nextcloud-Einstieg vorhanden; Monatsplan vollständig geladen und ohne OrgSuite bedienbar. Keine Fehler oder fehlenden Komponenten festgestellt. |
+| A2 | Suite-Einstieg | AdPlaner mit aktiver OrgSuite über den AD-Einstieg öffnen und zwischen aktivierten AD-Apps wechseln. | Es gibt keinen doppelten Haupteinstieg; AdPlaner ist im gemeinsamen Menü korrekt markiert. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Gemeinsamer AD-Einstieg vorhanden; kein doppelter Haupteinstieg; AdPlaner im gemeinsamen Menü sichtbar und korrekt markiert. Wechsel zwischen AD-Apps und Laden des Monatsplans funktionieren. |
+| A3 | Team und Monat | Zwischen mindestens zwei synthetischen Teams sowie vorherigem und nächstem Monat wechseln. | Auswahl, Überschrift, Tage, Schichten und Zuweisungen gehören stets zum gewählten Team und Monat. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Wechsel zwischen zwei Teams sowie vorherigem und nächstem Monat funktionierte korrekt. Überschrift, Tage, Schichten und Zuweisungen gehörten jeweils zum gewählten Team und Monat. Keine vermischten oder veralteten Daten sichtbar. |
+| A4 | Monatsgrenzen | Februar sowie einen Monats-/Jahreswechsel öffnen. | Kalendertage und gespeicherte Planwerte werden ohne fehlende oder doppelte Tage angezeigt. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Februar einschließlich Schaltjahr sowie Monats-/Jahreswechsel geprüft. Keine fehlenden oder doppelten Tage; gespeicherte Planwerte korrekt. Verbesserungsbedarf: Es gibt keine Schalter für „vorheriger Monat“ und „nächster Monat“; der Wechsel ist nur über das Datumsfeld möglich. |
+| A5 | Tastatur und Fokus | Team-, Monats-, Tab- und Plansteuerung nur mit Tastatur bedienen. | Alle Funktionen sind erreichbar, der Fokus ist sichtbar und die Tabs melden Auswahl und Zielbereich korrekt. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Teamauswahl, Monatssteuerung, Tabs und Plansteuerung waren per Tastatur bedienbar. Der Fokus war sichtbar, die aktive Tab-Auswahl erkennbar und es bestand keine Tastaturfalle. |
+| A6 | Responsivität und Scrollen | Viele Schichten und Personen bei kleinem Fenster anzeigen und horizontal sowie vertikal scrollen. | App-Inhalte bleiben erreichbar; der App-Root scrollt vertikal und breite Planungselemente sprengen nicht die Nextcloud-Seite. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Kleines Fenster geprüft; vertikales Scrollen funktioniert, alle Inhalte bleiben erreichbar und die Nextcloud-Seite wird nicht horizontal gesprengt. Keine doppelten oder überlagernden Scrollleisten. Verbesserungsbedarf: Die horizontale Scrollleiste ist erst am Ende des gesamten Inhalts erreichbar und sollte wie im AD Kalender am unteren Rand des sichtbaren Planbereichs verfügbar bleiben. |
+
+## B. Schichtkonfiguration und Wunschplanung
+
+| ID | Was wird geprüft? | Auszuführende Schritte | Erwartetes Ergebnis | Ergebnis | Warum/Beleg/Abweichung |
+|---|---|---|---|---|---|
+| B1 | Variable Schichtliste | Als zuständige EB eine Schicht ergänzen, umbenennen, zeitlich ändern, deaktivieren und wieder aktivieren. | Die strukturierte Teamkonfiguration bleibt nach Neuladen erhalten und steuert den Monatsplan. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Mit `adc-test-eb-sued` im Team `KaKü` wurde eine Schicht ergänzt, nach Neuladen erhalten, umbenannt, zeitlich geändert, deaktiviert und wieder aktiviert. Der Monatsplan reagierte korrekt. Offene UI-Punkte: Der Speicherbutton für Kommentare liegt rechts außerhalb des sichtbaren Bereichs. Die Mitarbeiterauswahl soll zunächst verborgen bleiben und erst nach Betätigung des `+`-Schalters als kompakte Buttonliste in einem Overlay erscheinen. |
+| B2 | Lücken und Überlappungen | Synthetische Schichten mit einer Lücke und einer zeitlichen Überlappung speichern. | Beide fachlich zulässigen Konfigurationen werden nicht vorschnell abgewiesen und erscheinen nachvollziehbar. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Schichtkonfigurationen mit zeitlicher Lücke und mit Überlappung wurden gespeichert und blieben nach Neuladen korrekt erhalten. Zeiten wurden nicht automatisch verändert. Testkonto und Testteam nicht dokumentiert. |
+| B3 | Schicht über Mitternacht | Eine Nachtschicht mit Ende am Folgetag konfigurieren und im Monatsplan prüfen. | Die Schicht wird dem Ausgangstag eindeutig zugeordnet und mit ihrem Zeitraum verständlich dargestellt. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Die Nachtschicht wurde gespeichert, blieb nach dem Neuladen erhalten, war eindeutig dem Ausgangstag zugeordnet und als über Mitternacht laufend verständlich dargestellt. Es entstand kein doppelter Eintrag am Folgetag. Präzisierung: Eine gesonderte Darstellung am Folgetag oder Prüfung der Monatsgrenze ist fachlich nicht erforderlich; jede Schicht wird einmal dem Tag ihres Beginns zugeordnet, Monatspläne sind voneinander abgegrenzt. |
+| B4 | Eigener Wunsch | Als schichtfähiges Teammitglied einen eigenen Wunsch hinzufügen, Bemerkung speichern und den Wunsch wieder entfernen. | Nur der eigene Eintrag wird verändert; Status und Bemerkung bleiben nach Neuladen erhalten. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Mit `admin` als normalem Assistenz-Teammitglied im Team `KaKü` konnte der eigene Wunsch hinzugefügt, nach Neuladen erhalten und wieder entfernt werden. Fremde Einträge blieben unverändert. Präzisiertes Soll: Normale Teammitglieder dürfen keine Bemerkungen speichern; Bemerkungen sind ausschließlich durch die zuständige EB bearbeitbar. |
+| B5 | Fremdzuweisung durch EB | Als zuständige EB eine andere schichtfähige Person zuweisen und wieder entfernen. | Die Zuweisung ist möglich und betrifft ausschließlich das gewählte Team und die gewählte Schicht. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Fremdzuweisung durch eine zuständige EB insgesamt erfolgreich geprüft. Konkrete Testperson, Team, Schicht und Einzelschritte wurden nicht separat dokumentiert. Keine Abweichungen angegeben. |
+| B6 | EB nicht schichtfähig | Versuchen, das reine EB-Testkonto selbst einer Schicht zuzuweisen. | Das EB-Konto wird nicht als schichtfähige Person angeboten beziehungsweise serverseitig abgewiesen. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Das reine EB-Konto wurde in der Personenauswahl nicht angeboten; eine Zuweisung über die UI war nicht möglich. Direkte serverseitige Negativprüfung nicht durchgeführt. |
+| B7 | Planstatus | Als EB die angebotenen Statuswechsel einschließlich `planned` und `approved` durchführen; als normales Teammitglied wiederholen. | Nur die EB kann den Status wechseln; unberechtigte Versuche verändern den Plan nicht. | [ ] erfolgreich [x] nicht erfolgreich [ ] nicht geprüft | Es gibt derzeit keine Option für den Planstatus. Die zuständige EB muss den Monatsplan mindestens als `planned` und `approved` festschreiben können. Ein als `approved` festgeschriebener Plan darf anschließend nicht mehr verändert werden. Normale Teammitglieder dürfen den Status nicht ändern. |
+
+## C. Team- und Rechteabgrenzung
+
+| ID | Was wird geprüft? | Auszuführende Schritte | Erwartetes Ergebnis | Ergebnis | Warum/Beleg/Abweichung |
+|---|---|---|---|---|---|
+| C1 | Teammitgliedschaft | Mit einem neutralen Konto aus Team A Team A und Team B direkt aufrufen. | Nur der erlaubte Teamkontext ist nutzbar; ein direkter unberechtigter Request auf Team B wird serverseitig abgewiesen. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Der erlaubte Teamkontext war nutzbar; der Zugriff auf ein nicht erlaubtes Team wurde insgesamt erfolgreich als abgegrenzt bewertet. Testkonto, Teams und Einzelprüfungen wurden nicht dokumentiert. |
+| C2 | EB-Schnittmenge | Ein Testkonto nur in der EB-Rollengruppe, ein Konto nur im Team und ein Konto in beiden Gruppen vergleichen. | EB-Rechte entstehen nur aus der vorgesehenen Team-und-Rolle-Schnittmenge. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Die vorgesehenen Rechte entstanden nur bei der Kombination aus Teamzugehörigkeit und EB-Rolle. Als Nur-EB-Testkontext wurde `west` angegeben. Weitere Konten und Einzelprüfungen wurden nicht separat dokumentiert. |
+| C3 | Fremdänderung durch normales Mitglied | Als normales Teammitglied eine fremde Zuweisung über UI und direkten API-Aufruf ändern. | Beide Wege werden abgewiesen; der bestehende Eintrag bleibt unverändert. | [ ] erfolgreich [ ] nicht erfolgreich [x] nicht geprüft | Mit `admin` im Team `KaKü` war eine fremde Änderung über die UI nicht möglich und der bestehende Eintrag blieb unverändert. Die erforderliche serverseitige Prüfung über einen direkten API-Aufruf wurde nicht durchgeführt. |
+| C4 | Bereichs- und Teamtrennung | Zwei Teams mit ähnlich benannten neutralen Konten und unterschiedlichen Konfigurationen prüfen. | Zuweisungen, Schichten, Bemerkungen und Rechte werden nicht zwischen Teams vermischt. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Die Teams `HaHü` und `KaKü` wurden verglichen. Schichten, Zuweisungen, Bemerkungen und Rechte blieben getrennt. Auch nach Team- und Monatswechsel trat keine Vermischung auf. |
+| C5 | Demo-Pack-Schutz | Adminbereich öffnen, Installation ohne Bestätigung versuchen und anschließend nur in einer dafür vorgesehenen Testumgebung bestätigen. | Ohne ausdrückliche Bestätigung bleibt die Aktion gesperrt; es werden ausschließlich synthetische Konten und Teams verwendet. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Installation ohne Bestätigung wurde blockiert und erzeugte keine Daten. Nach ausdrücklicher Bestätigung war die Installation möglich. Es wurden ausschließlich synthetische Konten, Teams und Plandaten erzeugt; bestehende Daten blieben unverändert. Eine erneute Installation wurde sicher behandelt. |
+
+## D. Optionale Integrationen und Fehlerzustände
+
+| ID | Was wird geprüft? | Auszuführende Schritte | Erwartetes Ergebnis | Ergebnis | Warum/Beleg/Abweichung |
+|---|---|---|---|---|---|
+| D1 | Betrieb ohne AD Urlaub | AD Urlaub deaktiviert lassen und Monatsplan, Einstellungen und Zuweisungen prüfen. | Der Monatsplan bleibt vollständig nutzbar; fehlende Abwesenheitshinweise blockieren keine Aktion. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Bei deaktiviertem AD Urlaub wurden Monatsplan, Team- und Monatswechsel, Einstellungen, eigene Wünsche und Fremdzuweisungen vollständig genutzt. Die fehlende Integration erzeugte weder Fehler noch blockierte Aktionen. |
+| D2 | Betrieb ohne AD Kalender | AD Kalender deaktiviert lassen und dieselben Kernabläufe wiederholen. | Die Assistenzplanung bleibt nutzbar; der fehlende optionale Provider wird nicht als Planfehler behandelt. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Bei deaktiviertem AD Kalender blieben Monatsplan, Team- und Monatswechsel, Schichten, Einstellungen, eigene Wünsche und Fremdzuweisungen vollständig nutzbar. Der fehlende Kalenderprovider erzeugte weder Fehler noch blockierte Aktionen. Zusätzliche Beobachtung: Vorhandener Urlaub hatte zu diesem Zeitpunkt noch keine sichtbare Auswirkung auf AdPlaner; dies wurde in D3 bewertet. |
+| D3 | Optionale Hinweise | Mit aktivem, neutral vorbereitetem Urlaubs- oder Kalenderprovider dessen Hinweise im Monatsplan prüfen. | Hinweise werden read-only dargestellt und verändern weder Teamrechte noch die führenden AdPlaner-Daten. | [ ] erfolgreich [x] nicht erfolgreich [ ] nicht geprüft | AD Urlaub und AD Kalender waren aktiv. Ein vorhandener synthetischer Urlaub wurde im Monatsplan nicht als Hinweis dargestellt; die Read-only-Eigenschaft war daher nicht prüfbar. Ein Kalenderhinweis war nicht vorhanden. Teamrechte und führende AdPlaner-Daten wurden nicht verändert. |
+| D4 | Verständliche Validierung | Leere Namen, unvollständige Zeiten und ungültige Eingaben in Einstellungen und Planung versuchen. | Fehler werden am richtigen Kontext verständlich angezeigt; gültige bestehende Daten bleiben erhalten. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Leere Namen, unvollständige Zeiten und weitere ungültige Eingaben wurden verständlich und am richtigen Kontext abgewiesen. Bestehende gültige Daten blieben erhalten; die Eingaben konnten unmittelbar korrigiert werden. Fehlermeldungen erscheinen vor einem etwaigen Neuladen, sodass kein kommentarloser Reload erfolgt. |
+| D5 | Datensparsame Abnahme | Formular und Screenshots prüfen. | Es wurden nur synthetische Team-, Dienst- und Kontodaten dokumentiert. | [x] erfolgreich [ ] nicht erfolgreich [ ] nicht geprüft | Ausschließlich neutrale Testkonten sowie synthetische oder freigegebene Teams und Schichten dokumentiert. Keine personenbezogenen Bemerkungen, Gesundheits- oder Urlaubsdetails, Zugangsdaten, Tokens oder unzulässigen Echtdaten in Screenshots. |
+
+## Abschlussentscheidung
+
+| Feld | Eintrag |
+|---|---|
+| Anzahl erfolgreich | 20 |
+| Anzahl nicht erfolgreich | 2 |
+| Anzahl nicht geprüft | 1 |
+| Kritische Abweichungen / Ticketreferenzen | B7: Planstatus `planned`/`approved` und Änderungssperre fehlen. D3: Optionale Urlaubs- und Kalenderhinweise werden nicht wie vorgesehen read-only dargestellt. C3: Direkte serverseitige Negativprüfung einer Fremdänderung steht aus. Weitere UI-Punkte: Monatsnavigation A4, sichtbare horizontale Scrollleiste A6, Kommentar-Speicherbutton und Mitarbeiterauswahl B1. |
+| Erneute Prüfung erforderlich bis | Vor Freigabe einer abnahmefähigen Version; kein konkretes Datum festgelegt |
+| Gesamtentscheidung | [ ] abgenommen [ ] mit Auflagen abgenommen [x] nicht abgenommen |
+| Begründung der Gesamtentscheidung | Die zentrale Planfreigabe einschließlich Statusverwaltung und Änderungssperre fehlt. Außerdem werden optionale Urlaubs- beziehungsweise Kalenderhinweise nicht wie vorgesehen dargestellt. C3 ist serverseitig noch nicht vollständig geprüft. |
+| Name / Datum | Simon / 02.08.2026 |
+
+## Zu bearbeitende Punkte
+
+### Hohe Priorität
+
+1. **B7 – Planstatus ergänzen**
+ - Status `planned` und `approved` implementieren.
+ - Nur die zuständige EB darf den Status ändern.
+ - Ein `approved`-Plan muss gegen Änderungen an Schichten, Zuweisungen, Wünschen und Bemerkungen gesperrt sein.
+ - Status muss nach Neuladen erhalten bleiben.
+ - Ein ausdrücklicher, berechtigter Entsperr- oder Rücksetzweg muss festgelegt werden.
+
+2. **D3 – Optionale Hinweise darstellen**
+ - Vorhandenen Urlaub einer schichtfähigen Person am betroffenen Tag im Monatsplan sichtbar machen.
+ - Hinweise ausschließlich read-only darstellen.
+ - Optional vorhandene Kalenderhinweise ebenfalls read-only anzeigen.
+ - Hinweise dürfen weder Teamrechte noch führende AdPlaner-Daten verändern.
+ - Die fachliche Wirkung auf Wünsche und Zuweisungen ist noch ausdrücklich festzulegen.
+
+### Mittlere Priorität
+
+3. **A6 – Horizontale Scrollleiste erreichbar halten**
+ - Horizontale Scrollleiste am unteren Rand des sichtbaren Plan-Viewports bereitstellen.
+ - Umsetzung am Verhalten des AD Kalenders orientieren.
+
+4. **B1 – Kommentar-Speicherbutton im sichtbaren Bereich halten**
+ - Speicheraktion ohne horizontales Scrollen erreichbar machen.
+
+5. **B1 – Mitarbeiterauswahl verdichten**
+ - Mitarbeiterauswahl zunächst verbergen.
+ - Erst nach Betätigung des `+`-Schalters als kompakte Buttonliste in einem Overlay öffnen.
+
+### Niedrige Priorität / Nachprüfung
+
+6. **A4 – Monatsnavigation ergänzen**
+ - Schalter für vorherigen und nächsten Monat ergänzen.
+
+7. **B4 – Prüfkriterium und Dokumentation korrigieren**
+ - Normale Teammitglieder dürfen eigene Wünsche hinzufügen und entfernen.
+ - Bemerkungen bleiben ausschließlich durch die zuständige EB bearbeitbar.
+
+8. **C3 – Serverseitige Rechteprüfung nachholen**
+ - Fremdänderung durch ein normales Teammitglied über einen direkten API-Aufruf versuchen.
+ - Verifizieren, dass der Server die Änderung abweist und der bestehende Eintrag unverändert bleibt.
+
+## Automatisierter Nachweis nach der historischen Abnahme
+
+Die vorstehenden Ergebnisse und die Gesamtentscheidung bleiben als historischer
+manueller Stand vom 02.08.2026 unverändert. Für `0.4.0-rc.1` sind B7 und D3
+technisch umgesetzt und durch Service-, API-, JavaScript- und Layouttests
+abgesichert: Die zuständige EB kann ausschließlich die erlaubten Übergänge
+`draft` → `planned` → `approved` sowie die ausdrücklichen Rückwege ausführen,
+ein genehmigter Monatsplan ist serverseitig und in der Oberfläche gesperrt,
+und optionale Urlaubs-/Kalenderhinweise bleiben read-only. Der reale
+DDEV-Integrationslauf belegt Statuspersistenz und unzulässige Übergänge gegen
+die Nextcloud-34-Datenbank. Die selbstbereinigende DDEV-Rechtematrix deckt den
+direkten serverseitigen Negativfall aus C3 ab.
+
+Auch die technischen Korrekturen zu A4 und A6 sind automatisiert belegt:
+direkte Vor-/Zurücknavigation und ein begrenzter, horizontal wie vertikal
+scrollbarer Plan-Viewport mit sichtbarer rechter Aktionsspalte. Die manuelle
+visuelle und fachliche Wiederholungsabnahme sowie die konkrete fachliche
+Wirkung der optionalen Hinweise auf Wünsche und Zuweisungen bleiben vor einer
+Produktfreigabe erforderlich.
+
+Für `0.4.0-rc.2` belegen zusätzliche Repository-, Service-, Routen- und
+JavaScript-Tests die Serialisierung konkurrierender Planänderungen, strikte
+Eingabegrenzen, idempotente Erstschreibvorgänge, das datensparsame Entfernen
+geleerter Tagesbemerkungen, ausschließlich aktuell schichtfähige Personen in
+öffentlichen Teamantworten sowie UID-freie read-only Planungshinweise. Der
+technische Monats-Lock verändert keine personenbezogenen fachlichen
+Änderungsmetadaten. Unbekannte Planstatus bleiben auch in der Oberfläche
+gesperrt; Tabellenüberschriften und Bemerkungsfelder sind programmatisch
+beschriftet. Diese automatisierten Nachweise ersetzen die ausstehende visuelle
+und fachliche Wiederholungsabnahme nicht.
+
+Ein zusätzlicher selbstbereinigender Nextcloud-34-DDEV-Smoke bestätigt diese
+Datenschutzverträge über den echten Gruppen-, HTTP-/CSRF- und Datenbankpfad:
+Deaktivierte Konten und EB-Konten werden nicht als schichtfähig ausgeliefert,
+das Leeren einer Tagesbemerkung entfernt deren Datenbankzeile, und technische
+Monats-Locks überschreiben keine fachlichen Änderungsmetadaten. Die getrennte
+Cleanup-Nachprüfung fand anschließend weder synthetische AdPlaner-Daten noch
+die temporären Konten oder die temporäre Gruppe.
+
+Eine selbstbereinigende Headless-Chrome-Wiederholungsabnahme belegt zusätzlich
+die gerenderte und interaktive Oberfläche in Nextcloud 34 mit einem
+synthetischen EB- und einem normalen Teamkonto. Geprüft wurden direkte
+Monatsnavigation, Tastaturwechsel der Tabs, Fremdzuweisung durch EB, eigener
+Wunsch, persistierte Tagesbemerkung und Schichtkonfiguration, die Übergänge
+`draft` → `planned` → `approved` einschließlich Sperre und ausdrücklichem
+Entsperren, schreibgeschützte Einstellungen für normale Mitglieder sowie das
+reale vertikale und horizontale Scrollverhalten bei schmalem Viewport. Ein
+temporärer Sichtnachweis wurde nur im Testverzeichnis unter `/tmp` erzeugt und
+mit dem Browserprofil gelöscht. Eine unabhängige Nachprüfung fand danach keine
+synthetischen Konten oder über deren Bearbeiterkennung auffindbaren Plan- und
+Bemerkungsdaten. Nicht Bestandteil dieses Laufs war das Umschalten von
+OrgSuite oder optionalen Provider-Apps.
diff --git a/js/admin.js b/js/admin.js
index bbaf31a..a6f299a 100644
--- a/js/admin.js
+++ b/js/admin.js
@@ -7,13 +7,13 @@
const client = new window.LocalBase.api.ApiClient({ appId: 'adplaner' });
confirmation.addEventListener('change', () => { button.disabled = !confirmation.checked; });
button.addEventListener('click', async () => {
- if (!confirmation.checked) return;
+ if (!confirmation.checked || button.disabled) return;
button.disabled = true;
notice.hidden = false;
notice.className = 'adp-admin-notice';
notice.textContent = 'Demo-Pack wird geprüft und installiert …';
try {
- const response = await client.request('/api/admin/demo-pack/install', { method: 'POST', body: '{}' });
+ const response = await client.request('/api/admin/demo-pack/install', { method: 'POST', body: '{"confirmed":true}' });
notice.classList.add('is-success');
notice.textContent = `${response.result.teams.join(', ')} wurden als Demoteams angelegt.`;
confirmation.checked = false;
diff --git a/js/components/assignment-control.js b/js/components/assignment-control.js
index c17b9f9..4829b9c 100644
--- a/js/components/assignment-control.js
+++ b/js/components/assignment-control.js
@@ -6,22 +6,20 @@
const assistants = (team.assistants || []).filter(assistant => {
return assistant.canReceiveShifts !== false && !assigned.has(assistant.uid);
});
+ const pickerId = `adp-assignment-picker-${esc(slot.id)}`;
return `
-
-
-
-
+
+
+ ${assistants.length === 0 ? 'Keine Assistenz verfügbar' : assistants.map(assistant => option(assistant, slot.id)).join('')}
`;
}
- function option(assistant) {
- return ``;
+ function option(assistant, slotId) {
+ return ``;
}
function open(button) {
@@ -30,14 +28,33 @@
return;
}
- document.querySelectorAll('[data-assignment-picker]').forEach(picker => {
- picker.hidden = picker.dataset.assignmentPicker !== slotId;
+ const picker = document.querySelector(`[data-assignment-picker="${CSS.escape(slotId)}"]`);
+ const shouldOpen = !!picker && picker.hidden;
+ document.querySelectorAll('[data-assignment-picker]').forEach(candidate => {
+ candidate.hidden = true;
+ });
+ document.querySelectorAll('[data-assignment-trigger]').forEach(trigger => {
+ trigger.setAttribute('aria-expanded', 'false');
});
+ if (!shouldOpen) {
+ return;
+ }
- const picker = document.querySelector(`[data-assignment-picker="${CSS.escape(slotId)}"]`);
- const select = picker ? picker.querySelector('select') : null;
- if (select) {
- select.focus();
+ picker.hidden = false;
+ button.setAttribute('aria-expanded', 'true');
+ if (!picker.dataset.escapeBound) {
+ picker.addEventListener('keydown', event => {
+ if (event.key !== 'Escape') return;
+ event.preventDefault();
+ picker.hidden = true;
+ button.setAttribute('aria-expanded', 'false');
+ button.focus();
+ });
+ picker.dataset.escapeBound = 'true';
+ }
+ const firstOption = picker ? picker.querySelector('button[data-action="add-selected"]') : null;
+ if (firstOption) {
+ firstOption.focus();
}
}
diff --git a/js/components/candidate-chip.js b/js/components/candidate-chip.js
index b937c79..c780b8d 100644
--- a/js/components/candidate-chip.js
+++ b/js/components/candidate-chip.js
@@ -1,13 +1,14 @@
(function() {
const { esc } = window.ADPlaner.ui;
- function render(candidate, canCoordinate, slotId) {
- const removable = canCoordinate || candidate.isSelf;
+ function render(candidate, canCoordinate, slotId, mutable = true) {
+ const removable = mutable && (canCoordinate || candidate.isSelf);
+ const displayName = candidate.displayName || candidate.uid;
return `
- ${esc(candidate.displayName || candidate.uid)}
- ${removable ? `` : ''}
+ ${esc(displayName)}
+ ${removable ? `` : ''}
`;
}
diff --git a/js/components/day-note-control.js b/js/components/day-note-control.js
index e969f27..192a5d8 100644
--- a/js/components/day-note-control.js
+++ b/js/components/day-note-control.js
@@ -1,5 +1,5 @@
(function() {
- const { esc } = window.ADPlaner.ui;
+ const { esc, dateShort } = window.ADPlaner.ui;
function render(day, canCoordinate) {
if (!canCoordinate) {
@@ -7,8 +7,8 @@
}
return `
-
-
+
+
`;
}
diff --git a/js/components/month-plan.js b/js/components/month-plan.js
index 3aef0f5..7787032 100644
--- a/js/components/month-plan.js
+++ b/js/components/month-plan.js
@@ -12,24 +12,29 @@
const team = plan.team;
const segments = plan.segments || [];
const canCoordinate = !!team.canCoordinate;
+ const status = typeof plan.status === 'string' ? plan.status : '';
+ const mutable = status === 'draft' || status === 'planned';
return `