From 653d59fc180342f5de88fdcb2831afa0cf7bf56c Mon Sep 17 00:00:00 2001 From: philipp lang Date: Mon, 23 Sep 2024 02:03:15 +0200 Subject: [PATCH] --wip-- [skip ci] --- app/Providers/LivewireServiceProvider.php | 25 +++ app/View/Ui/Box.php | 38 +++++ config/app.php | 3 +- config/livewire.php | 160 ++++++++++++++++++ modules/Dashboard/Block.php | 12 ++ .../Components/DashboardComponent.php | 33 ++++ modules/Dashboard/DashboardFactory.php | 48 ++++++ .../Dashboard/DashboardServiceProvider.php | 32 ++++ modules/Dashboard/Events/DashboardShowing.php | 52 ++++++ .../tests/DashboardComponentTest.php | 48 ++++++ modules/Invoice/MemberPaymentBlock.php | 42 +++++ modules/Member/AgeGroupCountBlock.php | 63 +++++++ modules/Member/TestersBlock.php | 48 ++++++ modules/Prevention/EfzPendingBlock.php | 49 ++++++ modules/Prevention/PsPendingBlock.php | 55 ++++++ .../views/components/layouts/app.blade.php | 3 + 16 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 app/Providers/LivewireServiceProvider.php create mode 100644 app/View/Ui/Box.php create mode 100644 config/livewire.php create mode 100644 modules/Dashboard/Block.php create mode 100644 modules/Dashboard/Components/DashboardComponent.php create mode 100644 modules/Dashboard/DashboardFactory.php create mode 100644 modules/Dashboard/DashboardServiceProvider.php create mode 100644 modules/Dashboard/Events/DashboardShowing.php create mode 100644 modules/Dashboard/tests/DashboardComponentTest.php create mode 100644 modules/Invoice/MemberPaymentBlock.php create mode 100644 modules/Member/AgeGroupCountBlock.php create mode 100644 modules/Member/TestersBlock.php create mode 100644 modules/Prevention/EfzPendingBlock.php create mode 100644 modules/Prevention/PsPendingBlock.php create mode 100644 resources/views/components/layouts/app.blade.php diff --git a/app/Providers/LivewireServiceProvider.php b/app/Providers/LivewireServiceProvider.php new file mode 100644 index 00000000..c4691d96 --- /dev/null +++ b/app/Providers/LivewireServiceProvider.php @@ -0,0 +1,25 @@ + !$second, + 'bg-gray-700 group-[.is-popup]:bg-zinc-600' => $second, + 'p-3 rounded-lg flex flex-col' => true + ])> +
+ @if($title) +
{{$title}}
+ @endif + {{$inTitle}} +
+
+ {{ $slot }} +
+ + HTML; + } +} diff --git a/config/app.php b/config/app.php index 5f2a4bbd..3e3f43c1 100644 --- a/config/app.php +++ b/config/app.php @@ -168,7 +168,6 @@ return [ */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\RouteServiceProvider::class, @@ -178,6 +177,8 @@ return [ App\Setting\SettingServiceProvider::class, App\Dashboard\DashboardServiceProvider::class, App\Providers\PluginServiceProvider::class, + // Modules\Dashboard\DashboardServiceProvider::class, + // App\Providers\LivewireServiceProvider::class, ], /* diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 00000000..e764241e --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,160 @@ + 'App\\View', + + /* + |--------------------------------------------------------------------------- + | View Path + |--------------------------------------------------------------------------- + | + | This value is used to specify where Livewire component Blade templates are + | stored when running file creation commands like `artisan make:livewire`. + | It is also used if you choose to omit a component's render() method. + | + */ + + 'view_path' => resource_path('views/livewire'), + + /* + |--------------------------------------------------------------------------- + | Layout + |--------------------------------------------------------------------------- + | The view that will be used as the layout when rendering a single component + | as an entire page via `Route::get('/post/create', CreatePost::class);`. + | In this case, the view returned by CreatePost will render into $slot. + | + */ + + 'layout' => 'components.layouts.app', + + /* + |--------------------------------------------------------------------------- + | Lazy Loading Placeholder + |--------------------------------------------------------------------------- + | Livewire allows you to lazy load components that would otherwise slow down + | the initial page load. Every component can have a custom placeholder or + | you can define the default placeholder view for all components below. + | + */ + + 'lazy_placeholder' => null, + + /* + |--------------------------------------------------------------------------- + | Temporary File Uploads + |--------------------------------------------------------------------------- + | + | Livewire handles file uploads by storing uploads in a temporary directory + | before the file is stored permanently. All file uploads are directed to + | a global endpoint for temporary storage. You may configure this below: + | + */ + + 'temporary_file_upload' => [ + 'disk' => null, // Example: 'local', 's3' | Default: 'default' + 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' + 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... + 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... + ], + + /* + |--------------------------------------------------------------------------- + | Render On Redirect + |--------------------------------------------------------------------------- + | + | This value determines if Livewire will run a component's `render()` method + | after a redirect has been triggered using something like `redirect(...)` + | Setting this to true will render the view once more before redirecting + | + */ + + 'render_on_redirect' => false, + + /* + |--------------------------------------------------------------------------- + | Eloquent Model Binding + |--------------------------------------------------------------------------- + | + | Previous versions of Livewire supported binding directly to eloquent model + | properties using wire:model by default. However, this behavior has been + | deemed too "magical" and has therefore been put under a feature flag. + | + */ + + 'legacy_model_binding' => false, + + /* + |--------------------------------------------------------------------------- + | Auto-inject Frontend Assets + |--------------------------------------------------------------------------- + | + | By default, Livewire automatically injects its JavaScript and CSS into the + | and of pages containing Livewire components. By disabling + | this behavior, you need to use @livewireStyles and @livewireScripts. + | + */ + + 'inject_assets' => true, + + /* + |--------------------------------------------------------------------------- + | Navigate (SPA mode) + |--------------------------------------------------------------------------- + | + | By adding `wire:navigate` to links in your Livewire application, Livewire + | will prevent the default link handling and instead request those pages + | via AJAX, creating an SPA-like effect. Configure this behavior here. + | + */ + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#2299dd', + ], + + /* + |--------------------------------------------------------------------------- + | HTML Morph Markers + |--------------------------------------------------------------------------- + | + | Livewire intelligently "morphs" existing HTML into the newly rendered HTML + | after each update. To make this process more reliable, Livewire injects + | "markers" into the rendered Blade surrounding @if, @class & @foreach. + | + */ + + 'inject_morph_markers' => true, + + /* + |--------------------------------------------------------------------------- + | Pagination Theme + |--------------------------------------------------------------------------- + | + | When enabling Livewire's pagination feature by using the `WithPagination` + | trait, Livewire will use Tailwind templates to render pagination views + | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" + | + */ + + 'pagination_theme' => 'tailwind', +]; 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..20ba4784 --- /dev/null +++ b/modules/Dashboard/DashboardFactory.php @@ -0,0 +1,48 @@ +> + */ + private array $blocks = [ + AgeGroupCountBlock::class, + MemberPaymentBlock::class, + TestersBlock::class, + EfzPendingBlock::class, + PsPendingBlock::class, + ]; + + /** + * @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; + + 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/Events/DashboardShowing.php b/modules/Dashboard/Events/DashboardShowing.php new file mode 100644 index 00000000..892d22f3 --- /dev/null +++ b/modules/Dashboard/Events/DashboardShowing.php @@ -0,0 +1,52 @@ + + */ + public array $blocks = []; + + /** + * Create a new event instance. + */ + public function __construct() + { + // + } + + public function purge(): void + { + $this->blocks = []; + } + + /** + * @param class-string $block + */ + public function push(string $block): void + { + $this->blocks[] = $block; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/modules/Dashboard/tests/DashboardComponentTest.php b/modules/Dashboard/tests/DashboardComponentTest.php new file mode 100644 index 00000000..7fd7256d --- /dev/null +++ b/modules/Dashboard/tests/DashboardComponentTest.php @@ -0,0 +1,48 @@ +login()->loginNami(); + + app(DashboardFactory::class)->purge(); + app(DashboardFactory::class)->register(ExampleBlock::class); + + Livewire::test(DashboardComponent::class) + ->assertSee('ExampleTitle') + ->assertSee('Example Content'); +}); + +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..76271371 --- /dev/null +++ b/modules/Invoice/MemberPaymentBlock.php @@ -0,0 +1,42 @@ + + */ + 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 component(): string + { + return 'member-payment'; + } + + 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..f09b6e34 --- /dev/null +++ b/modules/Member/AgeGroupCountBlock.php @@ -0,0 +1,63 @@ + + */ + 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 component(): string + { + return 'age-group-count'; + } + + public function title(): string + { + return 'Gruppierungs-Verteilung'; + } + + public function render(): string + { + return '
'; + } +} diff --git a/modules/Member/TestersBlock.php b/modules/Member/TestersBlock.php new file mode 100644 index 00000000..a4f516ae --- /dev/null +++ b/modules/Member/TestersBlock.php @@ -0,0 +1,48 @@ + + */ + 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 component(): string + { + return 'testers'; + } + + 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..01ef42a6 --- /dev/null +++ b/modules/Prevention/EfzPendingBlock.php @@ -0,0 +1,49 @@ + + */ + 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 component(): string + { + return 'efz-pending'; + } + + 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..2198bab9 --- /dev/null +++ b/modules/Prevention/PsPendingBlock.php @@ -0,0 +1,55 @@ + + */ + 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 component(): string + { + return 'ps-pending'; + } + + 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..4769b81e --- /dev/null +++ b/resources/views/components/layouts/app.blade.php @@ -0,0 +1,3 @@ + + {{ $slot }} +