diff --git a/app/Livewire/Party/Index.php b/app/Livewire/Party/Index.php new file mode 100644 index 0000000..ac079e1 --- /dev/null +++ b/app/Livewire/Party/Index.php @@ -0,0 +1,23 @@ +map(function ($party) { + return [ + 'code' => $party->value, + 'name' => $party->label(), + 'logo' => $party->logo(), + ]; + }); + + return view('livewire.party.index', compact('parties')) + ->title('Partier - Riksdagen App'); + } +} diff --git a/app/Livewire/Party/Show.php b/app/Livewire/Party/Show.php new file mode 100644 index 0000000..283ff0f --- /dev/null +++ b/app/Livewire/Party/Show.php @@ -0,0 +1,229 @@ +partyCode = strtoupper($partyCode); + $this->service = app(RiksdagenService::class); + + // Find party info + $this->party = collect(PartyEnum::cases()) + ->firstWhere('value', $this->partyCode); + + if (! $this->party) { + abort(404); + } + + $this->loadPartyData(); + } + + public function loadPartyData() + { + // Load party motions + $this->loadMotions(); + + // Load party members + $this->loadMembers(); + + // Load voting data for members + $this->loadVotingData(); + } + + public function loadMotions() + { + $result = Cache::remember('party_motions_'.$this->partyCode, 60 * 30, function () { + return $this->service->getPartyMotions($this->partyCode); + }); + + $this->motions = $result->original->dokumentlista->dokument ?? []; + + // Set default selected year if motions exist + if (! empty($this->motions)) { + $years = collect($this->motions)->pluck('rm')->unique()->sort()->reverse(); + $this->selectedYear = $years->first(); + } + } + + public function loadMembers() + { + $result = Cache::remember('party_members_'.$this->partyCode, 24 * 60 * 60, function () { + return $this->service->searchPerson(party: $this->partyCode); + }); + + $this->members = $result->original->personlista->person ?? []; + } + + public function loadVotingData() + { + // For now, load voting data for a sample of members due to API limitations + $sampleMembers = collect($this->members)->take(5); + + foreach ($sampleMembers as $member) { + $memberVotes = Cache::remember('member_votes_'.$member->intressent_id, 60 * 60, function () use ($member) { + return $this->service->searchVotes(mp_id: $member->intressent_id); + }); + + $votes = $memberVotes->original->voteringlista->votering ?? []; + $this->votes = array_merge($this->votes, is_array($votes) ? $votes : [$votes]); + } + } + + public function selectYear($year) + { + $this->selectedYear = $year; + } + + #[Computed()] + public function motionsByYear() + { + $grouped = []; + foreach ($this->motions as $motion) { + $rm = $motion->rm; + if (! isset($grouped[$rm])) { + $grouped[$rm] = []; + } + $grouped[$rm][] = $motion; + } + + return $grouped; + } + + #[Computed()] + public function motionStatistics() + { + $stats = [ + 'total' => count($this->motions), + 'by_year' => [], + 'by_type' => [], + ]; + + foreach ($this->motions as $motion) { + // By year + $year = $motion->rm; + $stats['by_year'][$year] = ($stats['by_year'][$year] ?? 0) + 1; + + // By type + $type = $motion->subtyp ?? 'Okänd'; + $stats['by_type'][$type] = ($stats['by_type'][$type] ?? 0) + 1; + } + + return $stats; + } + + #[Computed()] + public function votingStatistics() + { + $stats = []; + foreach ($this->votes as $vote) { + $voteType = $vote->rost ?? 'Okänd'; + $stats[$voteType] = ($stats[$voteType] ?? 0) + 1; + } + + return $stats; + } + + #[Computed()] + public function motionsColumnChart() + { + $motionStats = $this->motionStatistics; + + $chart = (new ColumnChartModel) + ->setTitle('Motioner per år') + ->setAnimated(true) + ->withDataLabels(); + + $colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']; + $colorIndex = 0; + + foreach ($motionStats['by_year'] as $year => $count) { + $chart->addColumn($year, $count, $colors[$colorIndex % count($colors)]); + $colorIndex++; + } + + return $chart; + } + + #[Computed()] + public function motionTypePieChart() + { + $motionStats = $this->motionStatistics; + + $chart = (new PieChartModel) + ->setTitle('Motioner per typ') + ->setAnimated(true) + ->withDataLabels(); + + $colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444']; + $colorIndex = 0; + + foreach ($motionStats['by_type'] as $type => $count) { + $chart->addSlice($type, $count, $colors[$colorIndex % count($colors)]); + $colorIndex++; + } + + return $chart; + } + + #[Computed()] + public function votingPieChart() + { + $votingStats = $this->votingStatistics; + + $chart = (new PieChartModel) + ->setTitle('Röstfördelning (urval av ledamöter)') + ->setAnimated(true) + ->withDataLabels(); + + $colors = [ + 'Ja' => '#10b981', + 'Nej' => '#ef4444', + 'Frånvarande' => '#6b7280', + 'Avstår' => '#f59e0b', + ]; + + foreach ($votingStats as $voteType => $count) { + $color = $colors[$voteType] ?? '#8b5cf6'; + $chart->addSlice($voteType, $count, $color); + } + + return $chart; + } + + public function placeholder() + { + return view('livewire.person.show-skeleton'); + } + + public function render() + { + return view('livewire.party.show') + ->title($this->party->label().' - Riksdagen App'); + } +} diff --git a/resources/views/livewire/home-page.blade.php b/resources/views/livewire/home-page.blade.php index 2a646ed..a225e10 100644 --- a/resources/views/livewire/home-page.blade.php +++ b/resources/views/livewire/home-page.blade.php @@ -123,6 +123,55 @@ + + +
+
+
+ + + +
+

+ Partier +

+

+ Utforska partiernas motioner, ledamöter och röststatistik +

+
+ Visa partier + + + +
+
+
+
+
- -
-
-
- - - -
-

- Partier -

-

- Utforska partier och deras ståndpunkter i olika frågor -

-
- Kommer snart -
-
-
-
+
+ + + + +
+

Sveriges Riksdag - Partier

+

Utforska information om de politiska partierna i Sveriges riksdag

+
+ + + +
+
\ No newline at end of file diff --git a/resources/views/livewire/party/show.blade.php b/resources/views/livewire/party/show.blade.php new file mode 100644 index 0000000..57478d4 --- /dev/null +++ b/resources/views/livewire/party/show.blade.php @@ -0,0 +1,256 @@ +
+
+ + + + +
+
+
+ @if ($party->logo()) + {{ $party->label() }} + @else + {{ $party->value }} + @endif +
+
+

{{ $party->label() }}

+

{{ $party->value }}

+
+
+
Totalt motioner
+
{{ $this->motionStatistics['total'] }}
+
+
+
Ledamöter
+
{{ count($members) }}
+
+
+
Röster (urval)
+
{{ count($votes) }}
+
+
+
+
+
+ + +
+ +
+
+
+ + + + +
+

Motioner per år

+
+ @if (!empty($this->motionStatistics['by_year'])) +
+ +
+ @else +

Ingen motionsstatistik tillgänglig

+ @endif +
+ + +
+
+
+ + + + +
+

Motionstyper

+
+ @if (!empty($this->motionStatistics['by_type'])) +
+ +
+ @else +

Ingen motionsstatistik tillgänglig

+ @endif +
+
+ + + @if (!empty($this->votingStatistics)) +
+
+
+ + + + +
+

Röststatistik (urval av ledamöter)

+
+
+
+ +
+
+
+ @php($totalVotes = array_sum($this->votingStatistics)) + @foreach ($this->votingStatistics as $voteType => $count) +
+ {{ $voteType }} + + {{ $totalVotes > 0 ? round(($count / $totalVotes) * 100, 2) : 0 }}% + +
+ @endforeach +
+
+
+
+ @endif + + + @if (!empty($this->motionsByYear)) +
+
+
+ + + + +
+

Partiets motioner

+
+ + +
+ +
+ + @if ($selectedYear && isset($this->motionsByYear[$selectedYear])) +
+ @foreach (collect($this->motionsByYear[$selectedYear])->take(10) as $motion) +
+
+
+

+ + {{ $motion->titel }} + +

+ @if (!empty($motion->undertitel)) +

{{ $motion->undertitel }}

+ @endif +
+ {{ $motion->rm }}:{{ $motion->beteckning }} + {{ \Carbon\Carbon::parse($motion->datum)->format('Y-m-d') }} + {{ $motion->subtyp }} +
+
+ +
+
+ @endforeach + + @if (count($this->motionsByYear[$selectedYear]) > 10) + + @endif +
+ @endif +
+ @endif + + + @if (!empty($members)) +
+
+
+ + + + +
+

Partiets ledamöter ({{ count($members) }})

+
+ +
+ @endif +
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 8b88090..e448589 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,8 @@ use App\Livewire\HomePage; use App\Livewire\Motion\Search as MotionSearch; use App\Livewire\Motion\Show as MotionShow; +use App\Livewire\Party\Index as PartyIndex; +use App\Livewire\Party\Show as PartyShow; use App\Livewire\Person\Search as PersonSearch; use App\Livewire\Person\Show as PersonShow; use App\Livewire\Settings\Appearance; @@ -20,6 +22,9 @@ Route::get('/ledamot/{personId}', PersonShow::class)->name('person.show'); Route::get('/motion', MotionSearch::class)->name('motion.search'); Route::get('/motion/{motionId}', MotionShow::class)->name('motion.show'); +Route::get('/parti', PartyIndex::class)->name('party.index'); +Route::get('/parti/{partyCode}', PartyShow::class)->name('party.show'); + Route::view('dashboard', 'dashboard') ->middleware(['auth', 'verified']) ->name('dashboard');