From 80b28ba2f89f5d7df5a2df0ba616ec0d8faeb504 Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 15 Jul 2026 19:11:41 +0200 Subject: [PATCH 1/4] feat: support standalone planner delivery --- AGENTS.md | 5 ++-- CHANGELOG.md | 6 +++++ README.md | 10 ++++---- appinfo/info.xml | 4 +--- lib/AppInfo/Application.php | 18 ++++++++++++++- .../IntegrationCapabilityQueryListener.php | 19 +++++++++++++++ lib/Listener/StandaloneNavigationListener.php | 21 +++++++++++++++++ templates/index.php | 2 -- tests/IntegrationCapabilityListenerTest.php | 23 +++++++++++++++++++ tests/StandaloneNavigationListenerTest.php | 17 ++++++++++++++ tests/Ui/SuiteNavigationSmokeTest.php | 5 ++-- 11 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 lib/Listener/IntegrationCapabilityQueryListener.php create mode 100644 lib/Listener/StandaloneNavigationListener.php create mode 100644 tests/IntegrationCapabilityListenerTest.php create mode 100644 tests/StandaloneNavigationListenerTest.php diff --git a/AGENTS.md b/AGENTS.md index 2c4882e..ac7d8eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,8 +101,9 @@ Diese Regeln gelten sinngemaess auch fuer andere eigene Nextcloud-Apps; die fach ### Gemeinsame Suite-Navigation -- AdPlaner besitzt keinen eigenen Nextcloud-Hauptnavigationseintrag. `orgsuite` stellt den gemeinsamen Einstieg `AD` bereit. -- Das Template bindet das zentrale OrgSuite-Menue mit `data-suite="ad"` und `data-current-app="adplaner"` ein. +- Ohne aktive OrgSuite registriert AdPlaner einen eigenen Nextcloud-Hauptnavigationseintrag. Ab zwei AD-Produkten ersetzt `orgsuite` diesen durch den gemeinsamen Einstieg `AD`. +- Das Template stellt den optionalen Menühost mit `data-suite="ad"` und `data-current-app="adplaner"` bereit, lädt aber keine OrgSuite-Assets direkt. +- Ohne AD Urlaub oder AD Kalender bleibt die Assistenzplanung eigenständig nutzbar; optionale Abwesenheits- und Konflikthinweise dürfen den Monatsplan nicht blockieren. - Team- und Planungsrechte bleiben ausschliesslich serverseitig im AdPlaner; Menuesichtbarkeit ist keine Berechtigung. - Der deckende Hintergrund und das vertikale Scrolling liegen am App-Root `#adplaner-app`; globale Nextcloud-Container wie `#content` werden nicht ueberschrieben. diff --git a/CHANGELOG.md b/CHANGELOG.md index c73db0f..de4616b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.0-rc.1 + +- Eigenständige Navigation ohne OrgSuite ergänzt. +- Assistenzplanfähigkeit über den optionalen LocalBase-Integrationsvertrag veröffentlicht. +- Ungültige harte App-Abhängigkeiten aus den Nextcloud-Metadaten entfernt. + ## 0.2.9-rc.1 - Öffentliche Projekt-, Quellcode- und Fehlerkanäle ergänzt. diff --git a/README.md b/README.md index b898e63..096cf33 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,14 @@ Monatliche Wunschdienstplanung für Assistenzteams. Urlaubsplanung liegt ausschl - Nextcloud 34 - PHP 8.3 oder neuer innerhalb des von Nextcloud 34 unterstützten Bereichs -- Abhängigkeiten: `localbase`, `orgsuite` +- Laufzeitbasis: `localbase`; `orgsuite` ist ab zwei AD-Fachprodukten optional aktiv - App-ID und Installationsordner: `adplaner` ## Installation -```bash -sudo -u www-data php occ app:enable localbase -sudo -u www-data php occ app:enable orgsuite -sudo -u www-data php occ app:enable adplaner -``` +Für Staging und Auslieferung das Produktbundle `ad-product-adplaner-.tar.gz` und dessen enthaltenes `install.sh` verwenden. Es prüft und installiert LocalBase automatisch; ab dem zweiten AD-Fachprodukt aktiviert es OrgSuite. + +AdPlaner funktioniert einzeln; optionale Abwesenheits- oder Kalenderhinweise entfallen ohne die jeweilige Fachapp, ohne den Monatsplan zu blockieren. Assistenzteams werden aus den zentral konfigurierten Nextcloud-Gruppen abgeleitet. Teambezogene Schichtkonfigurationen werden durch berechtigte Einsatzbegleitungen gepflegt. diff --git a/appinfo/info.xml b/appinfo/info.xml index 02694ce..f446128 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Assistenz Dienstplanung Wunschdienstplanung für Assistenzteams. Verwaltet monatliche Wunschdienstpläne für Assistenzteams auf Basis dynamischer Nextcloud-Gruppen. - 0.2.9-rc.1 + 0.3.0-rc.1 agpl Simon https://github.com/Filzmann/ad-suite @@ -16,7 +16,5 @@ - localbase - orgsuite diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a943175..f9a3130 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -4,12 +4,28 @@ namespace OCA\AdPlaner\AppInfo; +use OCA\AdPlaner\Listener\IntegrationCapabilityQueryListener; +use OCA\AdPlaner\Listener\StandaloneNavigationListener; +use OCA\LocalBase\Integration\IntegrationCapabilityQueryEvent; use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Navigation\Events\LoadAdditionalEntriesEvent; -class Application extends App { +/** Zweck: Registriert Assistenzplanfähigkeit und Standalone-Navigation im Nextcloud-Bootstrap. */ +class Application extends App implements IBootstrap { public const APP_ID = 'adplaner'; public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } + + public function register(IRegistrationContext $context): void { + $context->registerEventListener(IntegrationCapabilityQueryEvent::class, IntegrationCapabilityQueryListener::class); + $context->registerEventListener(LoadAdditionalEntriesEvent::class, StandaloneNavigationListener::class); + } + + public function boot(IBootContext $context): void { + } } diff --git a/lib/Listener/IntegrationCapabilityQueryListener.php b/lib/Listener/IntegrationCapabilityQueryListener.php new file mode 100644 index 0000000..42e59ba --- /dev/null +++ b/lib/Listener/IntegrationCapabilityQueryListener.php @@ -0,0 +1,19 @@ + */ +final class IntegrationCapabilityQueryListener implements IEventListener { + public function handle(Event $event): void { + if (!$event instanceof IntegrationCapabilityQueryEvent) return; + $event->provide(Application::APP_ID, [AdIntegrationCapabilities::ASSISTANT_SCHEDULE_READ]); + } +} diff --git a/lib/Listener/StandaloneNavigationListener.php b/lib/Listener/StandaloneNavigationListener.php new file mode 100644 index 0000000..1f8988a --- /dev/null +++ b/lib/Listener/StandaloneNavigationListener.php @@ -0,0 +1,21 @@ + */ +final class StandaloneNavigationListener implements IEventListener { + public function __construct(private StandaloneAppNavigationService $navigation) { + } + + public function handle(Event $event): void { + if (!$event instanceof LoadAdditionalEntriesEvent) return; + $this->navigation->addWhenStandalone('adplaner', 'Assistenzplanung', 'adplaner.page.index', 'app.svg', 82); + } +} diff --git a/templates/index.php b/templates/index.php index f1eb096..d23ea3c 100644 --- a/templates/index.php +++ b/templates/index.php @@ -2,7 +2,6 @@ \OCP\Util::addScript('localbase', 'api/api-client'); \OCP\Util::addScript('adplaner', 'modules/api'); \OCP\Util::addScript('localbase', 'ui/ui'); -\OCP\Util::addScript('orgsuite', 'suite-navigation'); \OCP\Util::addScript('adplaner', 'modules/ui'); \OCP\Util::addScript('localbase', 'models/model'); \OCP\Util::addScript('adplaner', 'models/assistant'); @@ -25,7 +24,6 @@ \OCP\Util::addScript('adplaner', 'modules/plan-app'); \OCP\Util::addScript('adplaner', 'main'); \OCP\Util::addStyle('adplaner', 'style'); -\OCP\Util::addStyle('orgsuite', 'suite-navigation'); ?>
diff --git a/tests/IntegrationCapabilityListenerTest.php b/tests/IntegrationCapabilityListenerTest.php new file mode 100644 index 0000000..cf1e852 --- /dev/null +++ b/tests/IntegrationCapabilityListenerTest.php @@ -0,0 +1,23 @@ +handle($event); + if ($event->providersFor(AdIntegrationCapabilities::ASSISTANT_SCHEDULE_READ) !== ['adplaner']) throw new RuntimeException('Assistenzplanfähigkeit fehlt.'); + if ($event->isAvailable(AdIntegrationCapabilities::ABSENCE_READ)) throw new RuntimeException('Assistenzplaner meldet eine fremde Fähigkeit.'); + + echo "AD Planer capability listener test passed\n"; +} diff --git a/tests/StandaloneNavigationListenerTest.php b/tests/StandaloneNavigationListenerTest.php new file mode 100644 index 0000000..fdf3308 --- /dev/null +++ b/tests/StandaloneNavigationListenerTest.php @@ -0,0 +1,17 @@ +user; } }; $apps = new class implements IAppManager { public function isEnabledForUser($appId, $user = null): bool { return false; } }; $nav = new class implements INavigationManager { public array $entries = []; public function add(callable $entry): void { $this->entries[] = $entry; } }; $url = new class implements IURLGenerator { public function linkToRoute(string $routeName, array $arguments = []): string { return $routeName; } public function imagePath(string $appName, string $file): string { return "$appName/$file"; } }; + (new StandaloneNavigationListener(new StandaloneAppNavigationService($session, $apps, $nav, $url)))->handle(new LoadAdditionalEntriesEvent()); $entry = ($nav->entries[0] ?? static fn(): array => [])(); + if (($entry['id'] ?? '') !== 'adplaner' || ($entry['name'] ?? '') !== 'Assistenzplanung' || ($entry['href'] ?? '') !== 'adplaner.page.index') throw new RuntimeException('Standalone-Planernavigation fehlt.'); + echo "AD Planer standalone navigation test passed\n"; +} diff --git a/tests/Ui/SuiteNavigationSmokeTest.php b/tests/Ui/SuiteNavigationSmokeTest.php index 665a5c0..dc09502 100644 --- a/tests/Ui/SuiteNavigationSmokeTest.php +++ b/tests/Ui/SuiteNavigationSmokeTest.php @@ -6,10 +6,11 @@ $css = file_get_contents(__DIR__ . '/../../css/style.css'); $info = file_get_contents(__DIR__ . '/../../appinfo/info.xml'); if ($template === false || $css === false || $info === false) throw new RuntimeException('AdPlaner-Vertragsdatei konnte nicht gelesen werden.'); -if (!str_contains($info, 'orgsuite') || str_contains($info, '')) throw new RuntimeException('OrgSuite-Appvertrag fehlt.'); -foreach (["\\OCP\\Util::addScript('orgsuite', 'suite-navigation')", "\\OCP\\Util::addStyle('orgsuite', 'suite-navigation')", 'data-orgsuite data-suite="ad" data-current-app="adplaner"'] as $contract) { +if (str_contains($info, '') || str_contains($info, '')) throw new RuntimeException('Standalone-Appvertrag fehlt.'); +foreach (['data-orgsuite data-suite="ad" data-current-app="adplaner"'] as $contract) { if (!str_contains($template, $contract)) throw new RuntimeException("Suite-Navigationsvertrag fehlt: {$contract}"); } +if (str_contains($template, "addScript('orgsuite'") || str_contains($template, "addStyle('orgsuite'")) throw new RuntimeException('Direkte OrgSuite-Assetkopplung vorhanden.'); foreach (['role="tablist"', 'role="tab"', 'aria-controls="adp-panel"', 'role="tabpanel"', 'aria-labelledby="adp-tab-month"'] as $contract) { if (!str_contains($template, $contract)) throw new RuntimeException("Semantischer Tabvertrag fehlt: {$contract}"); } From ff35bfc4880920aa788d9b1558882389a5c837d4 Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 15 Jul 2026 19:43:19 +0200 Subject: [PATCH 2/4] ci: resolve matching localbase branch --- .github/workflows/tests.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87c6d71..2170c07 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,10 +26,21 @@ jobs: uses: actions/checkout@v7 with: path: app + - name: LocalBase-Referenz bestimmen + id: localbase-ref + env: + CANDIDATE_REF: ${{ github.head_ref || github.ref_name }} + run: | + if git ls-remote --exit-code --heads https://github.com/Filzmann/nextcloud-localbase.git "refs/heads/$CANDIDATE_REF" >/dev/null 2>&1; then + echo "ref=$CANDIDATE_REF" >> "$GITHUB_OUTPUT" + else + echo "ref=main" >> "$GITHUB_OUTPUT" + fi - name: LocalBase auschecken uses: actions/checkout@v7 with: repository: Filzmann/nextcloud-localbase + ref: ${{ steps.localbase-ref.outputs.ref }} path: localbase - name: PHP einrichten uses: shivammathur/setup-php@v2 @@ -50,10 +61,21 @@ jobs: uses: actions/checkout@v7 with: path: app + - name: LocalBase-Referenz bestimmen + id: localbase-ref + env: + CANDIDATE_REF: ${{ github.head_ref || github.ref_name }} + run: | + if git ls-remote --exit-code --heads https://github.com/Filzmann/nextcloud-localbase.git "refs/heads/$CANDIDATE_REF" >/dev/null 2>&1; then + echo "ref=$CANDIDATE_REF" >> "$GITHUB_OUTPUT" + else + echo "ref=main" >> "$GITHUB_OUTPUT" + fi - name: LocalBase auschecken uses: actions/checkout@v7 with: repository: Filzmann/nextcloud-localbase + ref: ${{ steps.localbase-ref.outputs.ref }} path: localbase - name: Node.js einrichten uses: actions/setup-node@v6 From bc628685f7882bceecba5ceaaea2d28aaa0cddf8 Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 15 Jul 2026 21:01:15 +0200 Subject: [PATCH 3/4] feat: add admin demo pack for assist planning --- AGENTS.md | 2 + appinfo/info.xml | 7 ++++ appinfo/routes.php | 1 + css/admin.css | 6 +++ js/admin.js | 26 +++++++++++++ lib/Command/SeedDemoCommand.php | 21 +++++++++++ lib/Controller/DemoAdminController.php | 33 ++++++++++++++++ lib/Service/PlanerDemoPackService.php | 52 ++++++++++++++++++++++++++ lib/Settings/Admin.php | 16 ++++++++ lib/Settings/AdminSection.php | 18 +++++++++ templates/admin.php | 16 ++++++++ tests/Service/DemoPackContractTest.php | 21 +++++++++++ 12 files changed, 219 insertions(+) create mode 100644 css/admin.css create mode 100644 js/admin.js create mode 100644 lib/Command/SeedDemoCommand.php create mode 100644 lib/Controller/DemoAdminController.php create mode 100644 lib/Service/PlanerDemoPackService.php create mode 100644 lib/Settings/Admin.php create mode 100644 lib/Settings/AdminSection.php create mode 100644 templates/admin.php create mode 100644 tests/Service/DemoPackContractTest.php diff --git a/AGENTS.md b/AGENTS.md index ac7d8eb..e00340b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,8 @@ Die folgenden IDs sind initiale Standardwerte. Assistenzteam-Präfix, sichtbarer - EB-Rechte: Nutzer*innen, die zugleich in der Assistenznehmer-Gruppe und der gemeinsamen Rollengruppe `ad-EB` sind. Rollen-/Bereichskombinationen werden nicht als eigene Gruppen akzeptiert. - Bereichszuordnungen werden app-uebergreifend separat als `ad-Bereich-` gepflegt; kombinierte Rollen-/Bereichsgruppen werden dynamisch abgeleitet. - AdPlaner und AD Urlaub verwenden dieselben Assistenzteam-Gruppen; separate Suffix-Gruppen werden nicht unterstützt. +- Der app-eigene Adminabschnitt installiert Demo-Inhalte nur nach ausdrücklicher Bestätigung. Das Pack legt Team A, Team B und Team C mit ausschließlich synthetischen lokalen Konten und Standardschichten an; WordPress-Bestandsdaten werden nicht importiert. +- Fremde oder LDAP-verwaltete Konten werden nicht als Demokonten übernommen. Bestehende read-only LDAP-Team- oder Rollengruppen brechen die Demo-Installation im Preflight vor jeder Mutation ab. - Schichten werden ausschließlich über die strukturierte Schichtkonfiguration verwaltet. Frühere einzelne Legacy-Parameter für Früh-, Spät- oder Nachtschichten werden nicht weitergeführt. - Die Schichtkonfiguration eines Assistenzteams ist eine delegierte fachliche Teamkonfiguration und wird durch die zuständige EB im AdPlaner gepflegt. Sie ist keine ausschließlich für Nextcloud-Admins bestimmte organisationsweite Einstellung und gehört deshalb nicht in den Suite-Adminbereich. diff --git a/appinfo/info.xml b/appinfo/info.xml index f446128..55f37c0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -17,4 +17,11 @@ + + OCA\AdPlaner\Command\SeedDemoCommand + + + OCA\AdPlaner\Settings\Admin + OCA\AdPlaner\Settings\AdminSection + diff --git a/appinfo/routes.php b/appinfo/routes.php index 13a5041..0506afe 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -10,5 +10,6 @@ ['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' => 'demo_admin#install', 'url' => '/api/admin/demo-pack/install', 'verb' => 'POST'], ], ]; diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..5b59c81 --- /dev/null +++ b/css/admin.css @@ -0,0 +1,6 @@ +.adp-admin { max-width: 900px; } +.adp-admin-panel { padding: 18px; border: 1px solid var(--color-border); border-radius: var(--border-radius-large); background: var(--color-main-background); } +.adp-demo-confirm { display: block; margin: 16px 0 10px; } +.adp-admin-notice { padding: 10px 12px; border: 2px solid var(--color-primary-element); border-radius: var(--border-radius); background: var(--color-primary-element-light); color: var(--color-main-text); } +.adp-admin-notice.is-success { border-color: var(--color-success); } +.adp-admin-notice.is-error { border-color: var(--color-error); background: var(--color-error-hover); } diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..bbaf31a --- /dev/null +++ b/js/admin.js @@ -0,0 +1,26 @@ +(function() { + 'use strict'; + const confirmation = document.getElementById('adp-demo-confirm'); + const button = document.getElementById('adp-demo-install'); + const notice = document.getElementById('adp-demo-notice'); + if (!confirmation || !button || !notice) return; + const client = new window.LocalBase.api.ApiClient({ appId: 'adplaner' }); + confirmation.addEventListener('change', () => { button.disabled = !confirmation.checked; }); + button.addEventListener('click', async () => { + if (!confirmation.checked) 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: '{}' }); + notice.classList.add('is-success'); + notice.textContent = `${response.result.teams.join(', ')} wurden als Demoteams angelegt.`; + confirmation.checked = false; + } catch (error) { + notice.classList.add('is-error'); + notice.textContent = error.message || 'Das Demo-Pack konnte nicht installiert werden.'; + button.disabled = false; + } + }); +}()); diff --git a/lib/Command/SeedDemoCommand.php b/lib/Command/SeedDemoCommand.php new file mode 100644 index 0000000..505adfe --- /dev/null +++ b/lib/Command/SeedDemoCommand.php @@ -0,0 +1,21 @@ +setName('adplaner:demo:seed')->setDescription('Erzeugt Team A, Team B und Team C mit synthetischen Demokonten.'); } + protected function execute(InputInterface $input, OutputInterface $output): int { + $result = $this->demoPack->install(); + $output->writeln('' . implode(', ', $result['teams']) . ' als Demoteams synchronisiert.'); + return self::SUCCESS; + } +} diff --git a/lib/Controller/DemoAdminController.php b/lib/Controller/DemoAdminController.php new file mode 100644 index 0000000..a4a6d70 --- /dev/null +++ b/lib/Controller/DemoAdminController.php @@ -0,0 +1,33 @@ +isAdmin()) return new JSONResponse(['error' => 'Keine Berechtigung.'], Http::STATUS_FORBIDDEN); + try { + return new JSONResponse(['result' => $this->demoPack->install()]); + } catch (\Throwable $error) { + $this->logger->error('Assistenzplaner-Demo-Pack konnte nicht installiert werden.', ['exception' => $error]); + return new JSONResponse(['error' => $error->getMessage()], Http::STATUS_BAD_REQUEST); + } + } + private function isAdmin(): bool { + $user = $this->session->getUser(); + return $user !== null && $this->groups->isAdmin($user->getUID()); + } +} diff --git a/lib/Service/PlanerDemoPackService.php b/lib/Service/PlanerDemoPackService.php new file mode 100644 index 0000000..a9457b5 --- /dev/null +++ b/lib/Service/PlanerDemoPackService.php @@ -0,0 +1,52 @@ +} */ + public function install(): array { + $definition = $this->organization?->definition() ?? AdOrganizationDefinition::defaults(); + $teamCodes = ['A', 'B', 'C']; + $ebGroup = $definition->roleGroupId('eb'); + $fixtures = []; + foreach ($teamCodes as $index => $teamCode) { + $teamGroup = $definition->teamGroupPrefix() . $teamCode; + $coordinatorNames = ['Enna Busch', 'Emil Weber', 'Eda Sommer']; + $fixtures[] = [ + 'uid' => 'ad-demo-eb-' . strtolower($teamCode), + 'displayName' => $coordinatorNames[$index] . " (EB, Team {$teamCode})", + 'groups' => [$ebGroup, $teamGroup], + ]; + foreach ([1, 2] as $number) { + $fixtures[] = [ + 'uid' => 'ad-demo-assistenz-' . strtolower($teamCode) . $number, + 'displayName' => "Demo Assistenz {$teamCode}{$number} (Team {$teamCode})", + 'groups' => [$teamGroup], + ]; + } + } + + $accounts = $this->accounts->provision('ad-suite-demo', $fixtures); + foreach ($teamCodes as $teamCode) { + $this->settings->save($teamCode, "Team {$teamCode}", $this->shiftConfig->defaults()); + } + return ['accounts' => $accounts, 'teams' => $teamCodes]; + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php new file mode 100644 index 0000000..4e57e7e --- /dev/null +++ b/lib/Settings/Admin.php @@ -0,0 +1,16 @@ +url->imagePath(Application::APP_ID, 'app.svg'); } + public function getID(): string { return Application::APP_ID; } + public function getName(): string { return 'Assistenzplanung'; } + public function getPriority(): int { return 61; } +} diff --git a/templates/admin.php b/templates/admin.php new file mode 100644 index 0000000..400d251 --- /dev/null +++ b/templates/admin.php @@ -0,0 +1,16 @@ + +
+

Assistenzplanung

+
+

Demo-Pack

+

Das Pack legt Team A, Team B und Team C mit synthetischen Einsatzbegleitungen, Assistenzkräften und Standardschichten an. Es wird ausschließlich nach dieser Bestätigung installiert.

+

Fremde Konten und read-only LDAP-Gruppen werden vor der ersten Änderung abgewiesen.

+ + + +
+
diff --git a/tests/Service/DemoPackContractTest.php b/tests/Service/DemoPackContractTest.php new file mode 100644 index 0000000..d10cde5 --- /dev/null +++ b/tests/Service/DemoPackContractTest.php @@ -0,0 +1,21 @@ +provision('ad-suite-demo'", "['A', 'B', 'C']", 'TeamSettingsService', 'ShiftConfigService'] as $contract) if (!str_contains($service, $contract)) throw new RuntimeException("Planer-Demo-Vertrag fehlt: {$contract}"); +foreach (['PlanerDemoPackService', '->install()'] as $contract) if (!str_contains($command, $contract)) throw new RuntimeException("Demo-Command delegiert nicht: {$contract}"); +foreach (['/api/admin/demo-pack/install', "'verb' => 'POST'"] as $contract) if (!str_contains($routes, $contract)) throw new RuntimeException("Demo-Route fehlt: {$contract}"); +foreach (['OCA\\AdPlaner\\Command\\SeedDemoCommand', 'OCA\\AdPlaner\\Settings\\Admin', 'OCA\\AdPlaner\\Settings\\AdminSection'] as $contract) if (!str_contains($info, $contract)) throw new RuntimeException("Admin-/Command-Registrierung fehlt: {$contract}"); +foreach (['private function isAdmin()', '$this->groups->isAdmin(', 'Http::STATUS_FORBIDDEN'] as $contract) if (!str_contains($controller, $contract)) throw new RuntimeException("Adminschutz fehlt: {$contract}"); +if (str_contains($controller, 'NoCSRFRequired')) throw new RuntimeException('Demo-Installation umgeht CSRF.'); +foreach (['id="adp-demo-confirm"', 'id="adp-demo-install"', 'Team A, Team B und Team C'] as $contract) if (!str_contains($template, $contract)) throw new RuntimeException("Demo-Adminoberfläche fehlt: {$contract}"); + +echo "DemoPackContractTest: OK\n"; From adf234a89ecebaeecb252cc4a33122535538402b Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 15 Jul 2026 21:02:40 +0200 Subject: [PATCH 4/4] chore: bump release candidate to 0.3.0-rc.2 --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 55f37c0..8ba1330 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Assistenz Dienstplanung Wunschdienstplanung für Assistenzteams. Verwaltet monatliche Wunschdienstpläne für Assistenzteams auf Basis dynamischer Nextcloud-Gruppen. - 0.3.0-rc.1 + 0.3.0-rc.2 agpl Simon https://github.com/Filzmann/ad-suite