diff --git a/composer.json b/composer.json
index e9408df0..7380fee0 100644
--- a/composer.json
+++ b/composer.json
@@ -114,6 +114,7 @@
"autoload-dev": {
"psr-4": {
"Modules\\Dashboard\\Tests\\": "modules/dashboard/tests/",
+ "Modules\\Base\\Tests\\": "modules/base/tests/",
"Tests\\": "tests/",
"Zoomyboy\\LaravelNami\\Tests\\": "packages/laravel-nami/tests/"
}
diff --git a/config/app.php b/config/app.php
index a763ad6e..f567ace3 100644
--- a/config/app.php
+++ b/config/app.php
@@ -178,7 +178,7 @@ return [
// App\Dashboard\DashboardServiceProvider::class,
App\Providers\PluginServiceProvider::class,
Modules\Dashboard\DashboardServiceProvider::class,
- App\Providers\LivewireServiceProvider::class,
+ Modules\Base\BaseServiceProvider::class,
],
/*
diff --git a/app/Providers/LivewireServiceProvider.php b/modules/Base/BaseServiceProvider.php
similarity index 75%
rename from app/Providers/LivewireServiceProvider.php
rename to modules/Base/BaseServiceProvider.php
index 880ad376..0cb597eb 100644
--- a/app/Providers/LivewireServiceProvider.php
+++ b/modules/Base/BaseServiceProvider.php
@@ -1,11 +1,12 @@
register(AgeGroupCountBlock::class);
app(DashboardFactory::class)->register(MemberPaymentBlock::class);
@@ -29,6 +31,8 @@ class LivewireServiceProvider extends ServiceProvider
app(DashboardFactory::class)->register(EfzPendingBlock::class);
app(DashboardFactory::class)->register(PsPendingBlock::class);
+ Livewire::component('page.sidebar', Sidebar::class);
+
ComponentAttributeBag::macro('mergeWhen', function ($condition, $key, $attributes) {
/** @var ComponentAttributeBag */
$self = $this;
@@ -41,6 +45,7 @@ class LivewireServiceProvider extends ServiceProvider
*/
public function boot(): void
{
+ Livewire::component('pagesidebar', Sidebar::class);
//
}
}
diff --git a/modules/Base/Components/Page/Header.php b/modules/Base/Components/Page/Header.php
new file mode 100644
index 00000000..960223ad
--- /dev/null
+++ b/modules/Base/Components/Page/Header.php
@@ -0,0 +1,34 @@
+
+
+ {{ $beforeTitle ?? ''}}
+ {{ $title }}
+ {{ $toolbar ?? '' }}
+
+
+ @if ($closeable)
+
+
+
+ @endif
+ {{ $right ?? '' }}
+
+
+ HTML;
+ }
+}
diff --git a/modules/Base/Components/Page/Layout.php b/modules/Base/Components/Page/Layout.php
new file mode 100644
index 00000000..d0cb95d8
--- /dev/null
+++ b/modules/Base/Components/Page/Layout.php
@@ -0,0 +1,54 @@
+user()->firstname . ' ' . auth()->user()->lastname;
+ }
+
+ public function userAvatar(): string
+ {
+ return auth()->user()->getGravatarUrl();
+ }
+
+ public function render()
+ {
+ return <<<'HTML'
+
+
+
+
+
+
+
+
+ {{ $toolbar ?? ''}}
+
+
+ {{ $right ?? '' }}
+
+
+
+
+
{{ $userName() }}
+
+
+
+
+
+ {{ $slot }}
+
+
+ HTML;
+ }
+}
diff --git a/app/View/Page/MenuEntry.php b/modules/Base/Components/Page/MenuEntry.php
similarity index 94%
rename from app/View/Page/MenuEntry.php
rename to modules/Base/Components/Page/MenuEntry.php
index 260fc395..85ea81df 100644
--- a/app/View/Page/MenuEntry.php
+++ b/modules/Base/Components/Page/MenuEntry.php
@@ -1,6 +1,6 @@
login()->loginNami();
+
+ Livewire::test(DummyComponent::class)
+ ->assertSee('Testcontent')
+ ->assertSee(auth()->user()->lastname);
+});
+
+class DummyComponent extends Component
+{
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+
+ Testcontent
+
+
+ HTML;
+ }
+}
diff --git a/modules/Dashboard/Block.php b/modules/Dashboard/Block.php
new file mode 100644
index 00000000..3376e3f2
--- /dev/null
+++ b/modules/Dashboard/Block.php
@@ -0,0 +1,12 @@
+put('menu', 'dashboard');
+ session()->put('title', 'Dashboard');
+
+ $this->blocks = $factory->load();
+ }
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+
+ @foreach($this->blocks as $block)
+
+
+
+ @endforeach
+
+
+ HTML;
+ }
+}
diff --git a/modules/Dashboard/DashboardFactory.php b/modules/Dashboard/DashboardFactory.php
new file mode 100644
index 00000000..03fa0789
--- /dev/null
+++ b/modules/Dashboard/DashboardFactory.php
@@ -0,0 +1,41 @@
+>
+ */
+ private array $blocks = [];
+
+ /**
+ * @return array
+ */
+ public function load(): array
+ {
+ return collect($this->blocks)->map(fn ($block) => app($block))->toArray();
+ }
+
+ /**
+ * @param class-string $block
+ */
+ public function register(string $block): self
+ {
+ $this->blocks[] = $block;
+
+ $componentName = str($block)->replace('\\', '.')->explode('.')->map(fn ($part) => str($part)->lcfirst()->kebab())->implode('.');
+ Livewire::component($componentName, $block);
+
+ return $this;
+ }
+
+ public function purge(): self
+ {
+ $this->blocks = [];
+
+ return $this;
+ }
+}
diff --git a/modules/Dashboard/DashboardServiceProvider.php b/modules/Dashboard/DashboardServiceProvider.php
new file mode 100644
index 00000000..25613e67
--- /dev/null
+++ b/modules/Dashboard/DashboardServiceProvider.php
@@ -0,0 +1,32 @@
+singleton(DashboardFactory::class, fn () => new DashboardFactory());
+ }
+
+ /**
+ * Bootstrap services.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ app(Router::class)->middleware(['web', 'auth:web'])->group(function ($router) {
+ $router->get('/', DashboardComponent::class)->name('home');
+ });
+ }
+}
diff --git a/modules/Dashboard/tests/DashboardComponentTest.php b/modules/Dashboard/tests/DashboardComponentTest.php
new file mode 100644
index 00000000..d2e38264
--- /dev/null
+++ b/modules/Dashboard/tests/DashboardComponentTest.php
@@ -0,0 +1,47 @@
+login()->loginNami();
+
+ app(DashboardFactory::class)->purge();
+ app(DashboardFactory::class)->register(ExampleBlock::class);
+
+ Livewire::test(DashboardComponent::class)
+ ->assertSee('ExampleTitle');
+});
+
+it('renders page successfully', function () {
+ $this->login()->loginNami();
+
+ $this->get('/')->assertOk()->assertSee('Dashboard');
+});
+
+class ExampleBlock extends Block
+{
+
+ public function title(): string
+ {
+ return 'ExampleTitle';
+ }
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+ Example Content
+
+ HTML;
+ }
+}
diff --git a/modules/Invoice/MemberPaymentBlock.php b/modules/Invoice/MemberPaymentBlock.php
new file mode 100644
index 00000000..ecd0c7db
--- /dev/null
+++ b/modules/Invoice/MemberPaymentBlock.php
@@ -0,0 +1,37 @@
+
+ */
+ public function data(): array
+ {
+ $amount = InvoicePosition::whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
+ ->selectRaw('sum(price) AS price')
+ ->first();
+ $members = Member::whereHasPendingPayment()->count();
+
+ return [
+ 'members' => $members,
+ 'total_members' => Member::count(),
+ 'amount' => number_format((int) $amount->price / 100, 2, ',', '.') . ' €',
+ ];
+ }
+
+ public function title(): string
+ {
+ return 'Ausstehende Mitgliedsbeiträge';
+ }
+
+ public function render(): string
+ {
+ return '';
+ }
+}
diff --git a/modules/Member/AgeGroupCountBlock.php b/modules/Member/AgeGroupCountBlock.php
new file mode 100644
index 00000000..48d88d79
--- /dev/null
+++ b/modules/Member/AgeGroupCountBlock.php
@@ -0,0 +1,61 @@
+
+ */
+ public function memberQuery(): Builder
+ {
+ return Membership::select('subactivities.slug', 'subactivities.name')
+ ->selectRaw('COUNT(member_id) AS count')
+ ->join('activities', 'memberships.activity_id', 'activities.id')
+ ->join('subactivities', 'memberships.subactivity_id', 'subactivities.id')
+ ->isAgeGroup()
+ ->isMember()
+ ->active()
+ ->groupBy('subactivities.slug', 'subactivities.name')
+ ->orderBy('subactivity_id');
+ }
+
+ /**
+ * @return Builder
+ */
+ public function leaderQuery(): Builder
+ {
+ return Membership::selectRaw('"leiter" AS slug, "Leiter" AS name, COUNT(member_id) AS count')
+ ->join('activities', 'memberships.activity_id', 'activities.id')
+ ->join('subactivities', 'memberships.subactivity_id', 'subactivities.id')
+ ->active()
+ ->isLeader();
+ }
+
+ protected function data(): array
+ {
+ return [
+ 'groups' => [
+ ...$this->memberQuery()->get()->toArray(),
+ ...$this->leaderQuery()->get()->toArray(),
+ ],
+ ];
+ }
+
+ public function title(): string
+ {
+ return 'Gruppierungs-Verteilung';
+ }
+
+ public function render(): string
+ {
+ return '
+
+ lalala
+
';
+ }
+}
diff --git a/modules/Member/TestersBlock.php b/modules/Member/TestersBlock.php
new file mode 100644
index 00000000..e8fbd71e
--- /dev/null
+++ b/modules/Member/TestersBlock.php
@@ -0,0 +1,43 @@
+
+ */
+ public function query(): Builder
+ {
+ return Member::whereHas('memberships', fn ($q) => $q->isTrying())
+ ->with('memberships', fn ($q) => $q->isTrying());
+ }
+
+ /**
+ * @return array{members: array}
+ */
+ public function data(): array
+ {
+ return [
+ 'members' => $this->query()->get()->map(fn ($member) => [
+ 'name' => $member->fullname,
+ 'try_ends_at' => $member->memberships->first()->from->addWeeks(8)->format('d.m.Y'),
+ 'try_ends_at_human' => $member->memberships->first()->from->addWeeks(8)->diffForHumans(),
+ ])->toArray(),
+ ];
+ }
+
+ public function title(): string
+ {
+ return 'Endende Schhnupperzeiten';
+ }
+
+ public function render(): string
+ {
+ return '';
+ }
+}
diff --git a/modules/Prevention/EfzPendingBlock.php b/modules/Prevention/EfzPendingBlock.php
new file mode 100644
index 00000000..473ae64d
--- /dev/null
+++ b/modules/Prevention/EfzPendingBlock.php
@@ -0,0 +1,44 @@
+
+ */
+ public function query(): Builder
+ {
+ return Member::where(function ($query) {
+ return $query->where('efz', '<=', now()->subYears(5)->endOfYear())
+ ->orWhereNull('efz');
+ })
+ ->whereCurrentGroup()
+ ->orderByRaw('lastname, firstname')
+ ->whereHas('memberships', fn ($builder) => $builder->isLeader()->active());
+ }
+
+ /**
+ * @return array{members: array}
+ */
+ public function data(): array
+ {
+ return [
+ 'members' => $this->query()->get()->map(fn ($member) => $member->fullname)->toArray(),
+ ];
+ }
+
+ public function title(): string
+ {
+ return 'Ausstehende Führungszeugnisse';
+ }
+
+ public function render(): string
+ {
+ return '';
+ }
+}
diff --git a/modules/Prevention/PsPendingBlock.php b/modules/Prevention/PsPendingBlock.php
new file mode 100644
index 00000000..e6bef7e7
--- /dev/null
+++ b/modules/Prevention/PsPendingBlock.php
@@ -0,0 +1,50 @@
+
+ */
+ public function query(): Builder
+ {
+ return Member::where(function ($query) {
+ $time = now()->subYears(5)->endOfYear();
+
+ return $query
+ ->orWhere(fn ($query) => $query->whereNull('ps_at')->whereNull('more_ps_at'))
+ ->orWhere(fn ($query) => $query->whereNull('ps_at')->where('more_ps_at', '<=', $time))
+ ->orWhere(fn ($query) => $query->where('ps_at', '<=', $time)->whereNull('more_ps_at'))
+ ->orWhere(fn ($query) => $query->where('ps_at', '>=', $time)->where('more_ps_at', '<=', $time));
+ })
+ ->whereCurrentGroup()
+ ->orderByRaw('lastname, firstname')
+ ->whereHas('memberships', fn ($builder) => $builder->isLeader()->active());
+ }
+
+ /**
+ * @return array{members: array{fullname: string}}
+ */
+ public function data(): array
+ {
+ return [
+ 'members' => $this->query()->get()->map(fn ($member) => [
+ 'fullname' => $member->fullname,
+ ])->toArray(),
+ ];
+ }
+
+ public function title(): string
+ {
+ return 'Ausstehende Präventionsschulungen';
+ }
+
+ public function render(): string
+ {
+ return '';
+ }
+}
diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php
new file mode 100644
index 00000000..846ac352
--- /dev/null
+++ b/resources/views/components/layouts/app.blade.php
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ {{ $title ?? 'Page Title' }}
+
+
+ @if(auth()->id())
+
+ @endif
+ @vite('resources/livewire-js/app.js')
+
+
+
+ {{ $slot }}
+
+
+