diff --git a/resources/js/electionAdminCharts.js b/resources/js/electionAdminCharts.js index e1f0969..fcbe1db 100644 --- a/resources/js/electionAdminCharts.js +++ b/resources/js/electionAdminCharts.js @@ -4,6 +4,7 @@ export default (livewireComponent) => ({ plebs: livewireComponent.entangle('plebs', true), electionConfig: livewireComponent.entangle('electionConfig', true), votes: livewireComponent.entangle('votes', true), + boardVotes: livewireComponent.entangle('boardVotes', true), charts: {}, // Store chart instances hexToRGB(h) { @@ -24,19 +25,11 @@ export default (livewireComponent) => ({ init() { this.createChart('chart_presidency', 'presidency'); - this.createChart('chart_vice_president', 'vice_president'); - this.createChart('chart_finances', 'finances'); - this.createChart('chart_secretary', 'secretary'); - this.createChart('chart_press_officer', 'press_officer'); - this.createChart('chart_it_manager', 'it_manager'); + this.createChart('chart_board', 'board'); this.$watch('votes', () => { this.createChart('chart_presidency', 'presidency'); - this.createChart('chart_vice_president', 'vice_president'); - this.createChart('chart_finances', 'finances'); - this.createChart('chart_secretary', 'secretary'); - this.createChart('chart_press_officer', 'press_officer'); - this.createChart('chart_it_manager', 'it_manager'); + this.createChart('chart_board', 'board'); }); }, @@ -79,7 +72,12 @@ export default (livewireComponent) => ({ const config = this.electionConfig.find(config => config.type === type); const labels = config ? config.candidates.map(candidate => candidate.name) : []; const labelsPubkeys = config ? config.candidates.map(candidate => candidate.pubkey) : []; - const data = this.votes.find(vote => vote.type === type); + let data; + if (type === 'board') { + data = this.boardVotes.find(vote => vote.type === type); + } else { + data = this.votes.find(vote => vote.type === type); + } const findVoteCountInDataByLabelsPubkey = data ? labelsPubkeys.map(pubkey => data.votes[pubkey]?.count ?? 0) : labelsPubkeys.map(() => 0); console.log('findVoteCountInDataByLabelsPubkey', findVoteCountInDataByLabelsPubkey); diff --git a/resources/js/nostrApp.js b/resources/js/nostrApp.js index 75dda89..142a8c8 100644 --- a/resources/js/nostrApp.js +++ b/resources/js/nostrApp.js @@ -2,6 +2,7 @@ export default (livewireComponent) => ({ isAllowed: livewireComponent.entangle('isAllowed', true), signThisEvent: livewireComponent.entangle('signThisEvent'), + showLog: livewireComponent.entangle('showLog', true), init() { // on change of signThisEvent, call the method diff --git a/resources/views/pages/association/election/[Election:year].blade.php b/resources/views/pages/association/election/[Election:year].blade.php index 3aa700a..b2085d8 100644 --- a/resources/views/pages/association/election/[Election:year].blade.php +++ b/resources/views/pages/association/election/[Election:year].blade.php @@ -1,49 +1,46 @@ false]); -state(['currentPubkey' => null]); -state(['currentPleb' => null]); -state(['events' => []]); -state(['election' => fn() => $election]); -state(['plebs' => []]); -state(['search' => '']); -state(['signThisEvent' => '']); -state(['isNotClosed' => true]); +state([ + 'isAllowed' => false, + 'showLog' => false, + 'currentPubkey' => null, + 'currentPleb' => null, + 'events' => [], + 'boardEvents' => [], + 'election' => fn() => $election, + 'plebs' => [], + 'search' => '', + 'signThisEvent' => '', + 'isNotClosed' => true +]); mount(function () { $this->plebs = \App\Models\EinundzwanzigPleb::query() - ->with([ - 'profile', - ]) + ->with(['profile']) ->whereIn('association_status', [3, 4]) ->orderBy('association_status', 'desc') ->get() ->toArray(); $this->loadEvents(); + $this->loadBoardEvents(); if ($this->election->end_time->isPast() || !config('services.voting')) { $this->isNotClosed = false; } @@ -52,60 +49,59 @@ mount(function () { on([ 'nostrLoggedIn' => function ($pubkey) { $this->currentPubkey = $pubkey; - $this->currentPleb = \App\Models\EinundzwanzigPleb::query() - ->where('pubkey', $pubkey)->first(); - if($this->currentPleb->association_status->value < 3) { + $this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first(); + if ($this->currentPleb->association_status->value < 3) { return redirect()->route('association.profile'); } + $logPubkeys = [ + '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033', + '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279', + ]; + if (in_array($this->currentPubkey, $logPubkeys, true)) { + $this->showLog = true; + } $this->isAllowed = true; }, -]); - -on([ 'echo:votes,.newVote' => function () { $this->loadEvents(); + $this->loadBoardEvents(); } ]); updated([ 'search' => function ($value) { $this->plebs = \App\Models\EinundzwanzigPleb::query() - ->with([ - 'profile', - ]) + ->with(['profile']) ->whereIn('association_status', [3, 4]) ->where(fn($query) => $query ->where('pubkey', 'like', "%$value%") - ->orWhereHas('profile', function ($query) use ($value) { - $query->where('name', 'ilike', "%$value%"); - })) + ->orWhereHas('profile', fn($query) => $query->where('name', 'ilike', "%$value%"))) ->orderBy('association_status', 'desc') ->get() ->toArray(); - }, + } ]); $loadEvents = function () { + $this->events = $this->loadNostrEvents([32121]); +}; + +$loadBoardEvents = function () { + $this->boardEvents = $this->loadNostrEvents([2121]); +}; + +$loadNostrEvents = function ($kinds) { $subscription = new Subscription(); $subscriptionId = $subscription->setId(); - - $filter1 = new Filter(); - $filter1->setKinds([2121]); // You can add multiple kind numbers - $filters = [$filter1]; // You can add multiple filters. - - $requestMessage = new RequestMessage($subscriptionId, $filters); - - $relays = [ - new Relay(config('services.relay')), - ]; + $filter = new Filter(); + $filter->setKinds($kinds); + $requestMessage = new RequestMessage($subscriptionId, [$filter]); $relaySet = new RelaySet(); - $relaySet->setRelays($relays); - + $relaySet->setRelays([new Relay(config('services.relay'))]); $request = new Request($relaySet, $requestMessage); $response = $request->send(); - - $this->events = collect($response[config('services.relay')]) + return collect($response[config('services.relay')]) ->map(fn($event) => [ 'id' => $event->event->id, @@ -117,14 +113,18 @@ $loadEvents = function () { ])->toArray(); }; -$vote = function ($pubkey, $type) { +$vote = function ($pubkey, $type, $board = false) { if ($this->election->end_time->isPast()) { $this->isNotClosed = false; return; } $note = new NostrEvent(); - $note->setContent($pubkey . ',' . $type); - $note->setKind(2121); + $note->setKind($board ? 2121 : 32121); + if (!$board) { + $dTag = sprintf('%s,%s,%s', $this->currentPleb->pubkey, date('Y'), $type); + $note->setTags([['d', $dTag]]); + } + $note->setContent("$pubkey,$type"); $this->signThisEvent = $note->toJson(); }; @@ -144,30 +144,23 @@ $signEvent = function ($event) { $note->setTags($event['tags']); $note->setCreatedAt($event['created_at']); $eventMessage = new EventMessage($note); - $relayUrl = config('services.relay'); - $relay = new Relay($relayUrl); + $relay = new Relay(config('services.relay')); $relay->setMessage($eventMessage); - $result = $relay->send(); - - Broadcast::on('votes') - ->as('newVote') - ->sendNow(); + $relay->send(); + Broadcast::on('votes')->as('newVote')->sendNow(); }; ?> @volt -
+
@php $positions = [ 'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'], - 'vice_president' => ['icon' => 'fa-user-group-crown', 'title' => 'Vizepräsidium'], - 'finances' => ['icon' => 'fa-bitcoin-sign', 'title' => 'Finanzen'], - 'secretary' => ['icon' => 'fa-stapler', 'title' => 'Revisionsstelle'], - 'press_officer' => ['icon' => 'fa-newspaper', 'title' => 'Pressewart'], - 'it_manager' => ['icon' => 'fa-server', 'title' => 'Technikwart'], + 'board' => ['icon' => 'fa-users', 'title' => 'Vizepräsidium'], ]; $loadedEvents = collect($events) ->map(function($event) { @@ -195,9 +188,34 @@ $signEvent = function ($event) { ->sortByDesc('created_at') ->unique(fn ($event) => $event['pubkey'] . $event['type']) ->values(); + $loadedBoardEvents = collect($boardEvents) + ->map(function($event) { + $profile = \App\Models\Profile::query() + ->where('pubkey', $event['pubkey']) + ->first() + ?->toArray(); + $votedFor = \App\Models\Profile::query() + ->where('pubkey', str($event['content'])->before(',')->toString()) + ->first() + ?->toArray(); + + return [ + 'id' => $event['id'], + 'kind' => $event['kind'], + 'content' => $event['content'], + 'pubkey' => $event['pubkey'], + 'tags' => $event['tags'], + 'created_at' => $event['created_at'], + 'profile' => $profile, + 'votedFor' => $votedFor, + 'type' => str($event['content'])->after(',')->toString(), + ]; + }) + ->sortByDesc('created_at') + ->values(); @endphp - +
@@ -361,6 +379,33 @@ $signEvent = function ($event) { 'candidates' => $candidates, ]; }); + $electionConfigBoard = collect(json_decode($election->candidates, true, 512, JSON_THROW_ON_ERROR)) + ->map(function ($c) use ($loadedBoardEvents, $currentPubkey) { + $candidates = \App\Models\Profile::query() + ->whereIn('pubkey', $c['c']) + ->get() + ->map(function ($p) use ($loadedBoardEvents, $c, $currentPubkey) { + $votedClass = ' bg-green-500/20 text-green-700'; + $notVotedClass = ' bg-gray-500/20 text-gray-100'; + $hasVoted = $loadedBoardEvents + ->filter(fn($e) => $e['type'] === $c['type'] && $e['pubkey'] === $currentPubkey) + ->firstWhere('votedFor.pubkey', $p->pubkey); + + return [ + 'pubkey' => $p->pubkey, + 'name' => $p->name, + 'picture' => $p->picture, + 'votedClass' => $hasVoted ? $votedClass : $notVotedClass, + 'hasVoted' => $hasVoted, + ]; + }); + + return [ + 'type' => $c['type'], + 'c' => $c['c'], + 'candidates' => $candidates, + ]; + }); @endphp
@php $president = $positions['presidency']; + $board = $positions['board']; @endphp -
-
-
-
- +
+
+
+
+
+ +
+
+
+
+

{{ $president['title'] }}

+
+
+ @php + $votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', 'presidency'); + @endphp + @if($votedResult) + Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt + @else + Wähle deinen Kandidaten für das Präsidium. + @endif +
-
-
-
-

{{ $president['title'] }}

-
-
- @php - $votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', 'presidency'); - @endphp - @if($votedResult) - Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt - @else - Wähle deinen Kandidaten für das Präsidium. - @endif -
-
-
-
- @foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] as $c) -
-
- {{ $c['name'] }} - {{ $c['name'] }} +
+
+ @foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] as $c) +
+
+ {{ $c['name'] }} + {{ $c['name'] }} +
-
- @endforeach -
-
+ @endforeach +
+ +
-

- Bestätigung der Vorstandsmitglieder +

+ Wahl der übrigen Vorstandsmitglieder

-
+
- @foreach(collect($positions)->filter(fn($position, $type) => $type !== 'presidency') as $type => $position) - @if($electionConfig->firstWhere('type', $type)) -
-
-
-
- -
-
-
-
-

{{ $position['title'] }}

-
-
- @php - $votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', $type); - @endphp - @if($votedResult) - Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt - @else - Klicke auf den Kandidaten, um seine Position als Vorstandsmitglied zu bestätigen. - @endif -
-
-
-
- @foreach($electionConfig->firstWhere('type', $type)['candidates'] as $c) -
-
- {{ $c['name'] }} - {{ $c['name'] }} -
-
- @endforeach -
-
+
+
+
+
+ Klicke auf den Kandidaten, um seine Position als Vorstandsmitglied zu bestätigen.
- @endif - @endforeach +
+
+ @foreach($electionConfigBoard->firstWhere('type', 'board')['candidates'] as $c) +
+
+ {{ $c['name'] }} + {{ $c['name'] }} +
+
+ @endforeach +
+
+
+
@@ -522,10 +550,10 @@ $signEvent = function ($event) {
- {{-- -
--}} +
+
diff --git a/resources/views/pages/association/election/admin/[Election:year].blade.php b/resources/views/pages/association/election/admin/[Election:year].blade.php index d97ca1a..b4d2346 100644 --- a/resources/views/pages/association/election/admin/[Election:year].blade.php +++ b/resources/views/pages/association/election/admin/[Election:year].blade.php @@ -1,157 +1,157 @@ false]); -state(['currentPubkey' => null]); -state(['votes' => null]); -state(['events' => null]); -state(['election' => fn() => $election]); -state(['signThisEvent' => '']); state([ + 'isAllowed' => false, + 'currentPubkey' => null, + 'votes' => null, + 'boardVotes' => null, + 'events' => null, + 'boardEvents' => null, + 'election' => fn() => $election, + 'signThisEvent' => '', 'plebs' => fn() => \App\Models\EinundzwanzigPleb::query() - ->with([ - 'profile', - ]) + ->with(['profile']) ->whereIn('association_status', [3, 4]) ->orderBy('association_status', 'desc') ->get() ->toArray(), -]); -state([ - 'electionConfig' => function () { - return collect(json_decode($this->election->candidates, true, 512, JSON_THROW_ON_ERROR)) - ->map(function ($c) { - $candidates = \App\Models\Profile::query() - ->whereIn('pubkey', $c['c']) - ->get() - ->map(fn($p) - => [ - 'pubkey' => $p->pubkey, - 'name' => $p->name, - 'picture' => $p->picture, - ]); - - return [ - 'type' => $c['type'], - 'c' => $c['c'], - 'candidates' => $candidates, - ]; - }); - }, + 'electionConfig' => fn() + => collect(json_decode($this->election->candidates, true, 512, JSON_THROW_ON_ERROR)) + ->map(fn($c) + => [ + 'type' => $c['type'], + 'c' => $c['c'], + 'candidates' => \App\Models\Profile::query() + ->whereIn('pubkey', $c['c']) + ->get() + ->map(fn($p) + => [ + 'pubkey' => $p->pubkey, + 'name' => $p->name, + 'picture' => $p->picture, + ]), + ]), ]); -mount(function () { - $this->loadEvents(); - $this->loadVotes(); -}); +mount(fn() + => [ + $this->loadEvents(), + $this->loadBoardEvents(), + $this->loadVotes(), + $this->loadBoardVotes(), +]); on([ - 'nostrLoggedIn' => function ($pubkey) { - $this->currentPubkey = $pubkey; + 'nostrLoggedIn' => fn($pubkey) + => [ + $this->currentPubkey = $pubkey, $allowedPubkeys = [ '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033', - '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279' - ]; - if (!in_array($this->currentPubkey, $allowedPubkeys, true)) { - return redirect()->route('association.profile'); - } - $this->isAllowed = true; - }, -]); - -on([ - 'echo:votes,.newVote' => function () { - $this->loadEvents(); - $this->loadVotes(); - }, + '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279', + ], + !in_array($this->currentPubkey, $allowedPubkeys, true) ? redirect()->route( + 'association.profile', + ) : $this->isAllowed = true, + ], + 'echo:votes,.newVote' => fn() + => [ + $this->loadEvents(), + $this->loadBoardEvents(), + $this->loadVotes(), + $this->loadBoardVotes(), + ], ]); $loadVotes = function () { - $votes = collect($this->events) - ->map(function ($event) { - $votedFor = \App\Models\Profile::query() - ->where('pubkey', str($event['content'])->before(',')->toString()) - ->first(); - if (!$votedFor) { - Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, [ - '--pubkey' => str($event['content'])->before(',')->toString(), - ]); - $votedFor = \App\Models\Profile::query() - ->where('pubkey', str($event['content'])->before(',')->toString()) - ->first(); - } - $votedFor = $votedFor->toArray(); - - return [ - 'created_at' => $event['created_at'], - 'pubkey' => $event['pubkey'], - 'forpubkey' => $votedFor['pubkey'], - 'type' => str($event['content'])->after(',')->toString(), - ]; - }) + $this->votes = collect($this->events) + ->map(fn($event) + => [ + 'created_at' => $event['created_at'], + 'pubkey' => $event['pubkey'], + 'forpubkey' => $this->fetchProfile($event['content']), + 'type' => str($event['content'])->after(',')->toString(), + ]) ->sortByDesc('created_at') ->unique(fn($event) => $event['pubkey'] . $event['type']) ->values() - ->toArray(); - - $this->votes = collect($votes) ->groupBy('type') ->map(fn($votes) => [ 'type' => $votes[0]['type'], - 'votes' => collect($votes) - ->groupBy('forpubkey') - ->map(fn($group) => ['count' => $group->count()]) - ->toArray(), + 'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(), + ]) + ->values() + ->toArray(); +}; + +$loadBoardVotes = function () { + $this->boardVotes = collect($this->boardEvents) + ->map(fn($event) + => [ + 'created_at' => $event['created_at'], + 'pubkey' => $event['pubkey'], + 'forpubkey' => $this->fetchProfile($event['content']), + 'type' => str($event['content'])->after(',')->toString(), + ]) + ->sortByDesc('created_at') + ->values() + ->groupBy('type') + ->map(fn($votes) + => [ + 'type' => $votes[0]['type'], + 'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(), ]) ->values() ->toArray(); }; $loadEvents = function () { + $this->events = $this->loadNostrEvents([32121]); +}; + +$loadBoardEvents = function () { + $this->boardEvents = $this->loadNostrEvents([2121]); +}; + +$fetchProfile = function ($content) { + $pubkey = str($content)->before(',')->toString(); + $profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first(); + if (!$profile) { + Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, ['--pubkey' => $pubkey]); + $profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first(); + } + return $profile->pubkey; +}; + +$loadNostrEvents = function ($kinds) { $subscription = new Subscription(); $subscriptionId = $subscription->setId(); - - $filter1 = new Filter(); - $filter1->setKinds([2121]); // You can add multiple kind numbers - $filters = [$filter1]; // You can add multiple filters. - - $requestMessage = new RequestMessage($subscriptionId, $filters); - - $relays = [ - new Relay(config('services.relay')), - ]; + $filter = new Filter(); + $filter->setKinds($kinds); + $requestMessage = new RequestMessage($subscriptionId, [$filter]); $relaySet = new RelaySet(); - $relaySet->setRelays($relays); - + $relaySet->setRelays([new Relay(config('services.relay'))]); $request = new Request($relaySet, $requestMessage); $response = $request->send(); - - $this->events = collect($response[config('services.relay')]) + return collect($response[config('services.relay')]) ->map(fn($event) => [ 'id' => $event->event->id, @@ -160,8 +160,7 @@ $loadEvents = function () { 'pubkey' => $event->event->pubkey, 'tags' => $event->event->tags, 'created_at' => $event->event->created_at, - ]) - ->toArray(); + ])->toArray(); }; ?> @@ -171,11 +170,7 @@ $loadEvents = function () { @php $positions = [ 'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'], - 'vice_president' => ['icon' => 'fa-user-group-crown', 'title' => 'Vizepräsidium'], - 'finances' => ['icon' => 'fa-bitcoin-sign', 'title' => 'Finanzen'], - 'secretary' => ['icon' => 'fa-stapler', 'title' => 'Revisionsstelle'], - 'press_officer' => ['icon' => 'fa-newspaper', 'title' => 'Pressewart'], - 'it_manager' => ['icon' => 'fa-server', 'title' => 'Technikwart'], + 'board' => ['icon' => 'fa-users', 'title' => 'Vorstandsmitglieder'], ]; @endphp @@ -193,24 +188,39 @@ $loadEvents = function () { - -
- @foreach($positions as $key => $position) -
-
-

{{ $position['title'] }} -

-
-
- - -
-
- @endforeach + @php + $president = $positions['presidency']; + $board = $positions['board']; + @endphp + +
+
+
+

{{ $president['title'] }} +

+
+
+ + +
+
+
+
+

{{ $board['title'] }} +

+
+
+ + +
+