Add new models and page for motions

This commit is contained in:
2025-12-27 16:51:57 +01:00
parent 3ef975ad39
commit 45fe15eef2
36 changed files with 3753 additions and 355 deletions

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ImportRiksdagDataJob;
use Illuminate\Console\Command;
class ImportRiksdagData extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'riksdag:import
{file? : The path to the JSON file to import}
{--url= : URL to download ZIP file from}
{--queue : Run the import job in the background queue}';
/**
* The console command description.
*/
protected $description = 'Import Riksdag data from a JSON file or download and process a ZIP file from URL';
/**
* Execute the console command.
*/
public function handle()
{
$filePath = $this->argument('file');
$url = $this->option('url');
if (! $filePath && ! $url) {
$this->error('Either provide a file path or use --url option');
return 1;
}
if ($url) {
// Process ZIP from URL
$this->info("Starting ZIP download and import from: {$url}");
if ($this->option('queue')) {
ImportRiksdagDataJob::dispatch($url, true);
$this->info('ZIP import job has been queued. Check queue workers for progress.');
} else {
try {
$job = new ImportRiksdagDataJob($url, true);
$job->handle();
$this->info('ZIP import completed successfully!');
} catch (\Exception $e) {
$this->error('ZIP import failed: '.$e->getMessage());
return 1;
}
}
} else {
// Process single file
if (! file_exists($filePath)) {
$this->error("File not found: {$filePath}");
return 1;
}
// Validate JSON format
$jsonContent = file_get_contents($filePath);
$testDecode = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('Invalid JSON format: '.json_last_error_msg());
return 1;
}
$this->info("Starting import from: {$filePath}");
if ($this->option('queue')) {
ImportRiksdagDataJob::dispatch($filePath, false);
$this->info('Import job has been queued. Check queue workers for progress.');
} else {
try {
$job = new ImportRiksdagDataJob($filePath, false);
$job->handle();
$this->info('Import completed successfully!');
} catch (\Exception $e) {
$this->error('Import failed: '.$e->getMessage());
return 1;
}
}
}
return 0;
}
}

View File

@@ -0,0 +1,562 @@
<?php
namespace App\Jobs;
use App\Models\DokAktivitet;
use App\Models\DokBilaga;
use App\Models\DokForslag;
use App\Models\DokIntressent;
use App\Models\DokReferens;
use App\Models\Dokument;
use App\Models\DokUppgift;
use App\Models\Organ;
use App\Models\Person;
use App\Models\PersonUppdrag;
use App\Models\PersonUppgift;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use ZipArchive;
class ImportRiksdagDataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $url;
protected $isUrl;
protected $filePath;
public function __construct(string $urlOrPath, bool $isUrl = false)
{
$this->url = $urlOrPath;
$this->isUrl = $isUrl;
$this->filePath = $urlOrPath;
}
public function handle()
{
try {
if ($this->isUrl) {
$this->processZipFromUrl();
} else {
$this->processSingleFile($this->filePath);
}
} catch (\Exception $e) {
Log::error('Error in ImportRiksdagDataJob: '.$e->getMessage());
throw $e;
}
}
protected function processZipFromUrl()
{
Log::info('Starting download from: '.$this->url);
// Download the ZIP file
$response = Http::timeout(300)->get($this->url);
if (! $response->successful()) {
throw new \Exception('Failed to download ZIP file from: '.$this->url);
}
// Create temporary directory
$tempDir = storage_path('app/temp/riksdag_import_'.time());
$zipPath = $tempDir.'/download.zip';
if (! is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
// Save ZIP file
file_put_contents($zipPath, $response->body());
Log::info('ZIP file downloaded to: '.$zipPath);
// Extract ZIP file
$extractPath = $tempDir.'/extracted';
$this->extractZipFile($zipPath, $extractPath);
// Process all JSON files in extracted directory
$this->processJsonFiles($extractPath);
// Clean up temporary files
$this->cleanupTempDirectory($tempDir);
Log::info('ZIP processing completed successfully');
}
protected function extractZipFile($zipPath, $extractPath)
{
$zip = new ZipArchive;
$result = $zip->open($zipPath);
if ($result !== true) {
throw new \Exception('Failed to open ZIP file: '.$zipPath.' (Error code: '.$result.')');
}
if (! is_dir($extractPath)) {
mkdir($extractPath, 0755, true);
}
$zip->extractTo($extractPath);
$zip->close();
Log::info('ZIP file extracted to: '.$extractPath);
}
protected function processJsonFiles($directory)
{
$jsonFiles = glob($directory.'/*.json');
if (empty($jsonFiles)) {
// Check subdirectories
$subdirs = glob($directory.'/*', GLOB_ONLYDIR);
foreach ($subdirs as $subdir) {
$jsonFiles = array_merge($jsonFiles, glob($subdir.'/*.json'));
}
}
Log::info('Found '.count($jsonFiles).' JSON files to process');
foreach ($jsonFiles as $jsonFile) {
Log::info('Processing file: '.basename($jsonFile));
$this->processSingleFile($jsonFile);
}
}
protected function cleanupTempDirectory($tempDir)
{
$this->deleteDirectory($tempDir);
Log::info('Temporary directory cleaned up: '.$tempDir);
}
protected function deleteDirectory($dir)
{
if (! is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir.'/'.$file;
is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
}
rmdir($dir);
}
protected function processSingleFile($filePath)
{
try {
$jsonContent = file_get_contents($filePath);
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Invalid JSON format: '.json_last_error_msg());
}
DB::beginTransaction();
// Handle new JSON structure with dokumentstatus wrapper
if (isset($data['dokumentstatus'])) {
$this->processDocumentStatus($data['dokumentstatus']);
}
// Legacy support for old structure
elseif (isset($data['dokumentlista'])) {
$this->importDokumentData($data['dokumentlista']);
} elseif (isset($data['personlista'])) {
$this->importPersonData($data['personlista']);
} elseif (isset($data['organlista'])) {
$this->importOrganData($data['organlista']);
}
// Handle single document structure
elseif (isset($data['dokument'])) {
$this->importSingleDokument($data['dokument']);
}
DB::commit();
Log::info('Successfully imported data from: '.$filePath);
} catch (\Exception $e) {
DB::rollBack();
Log::error("Error importing data from {$filePath}: ".$e->getMessage());
throw $e;
}
}
protected function processDocumentStatus($dokumentStatus)
{
// Process the main document
if (isset($dokumentStatus['dokument'])) {
$this->processDokument($dokumentStatus['dokument']);
}
// Get hangar_id for related data processing
$hangarId = $dokumentStatus['dokument']['hangar_id'] ?? null;
if (! $hangarId) {
throw new \Exception('No hangar_id found in document data');
}
// Process document proposals (forslag)
if (isset($dokumentStatus['dokforslag']['forslag']) && is_array($dokumentStatus['dokforslag']['forslag'])) {
$this->processForslag($hangarId, $dokumentStatus['dokforslag']['forslag']);
}
// Process document activities (aktivitet)
if (isset($dokumentStatus['dokaktivitet']['aktivitet']) && is_array($dokumentStatus['dokaktivitet']['aktivitet'])) {
$this->processAktiviteter($hangarId, $dokumentStatus['dokaktivitet']['aktivitet']);
}
// Process document interested parties (intressent)
if (isset($dokumentStatus['dokintressent']['intressent']) && is_array($dokumentStatus['dokintressent']['intressent'])) {
$this->processIntressenter($hangarId, $dokumentStatus['dokintressent']['intressent']);
}
// Process document information (uppgift)
if (isset($dokumentStatus['dokuppgift']['uppgift']) && is_array($dokumentStatus['dokuppgift']['uppgift'])) {
$this->processUppgifter($hangarId, $dokumentStatus['dokuppgift']['uppgift']);
}
// Process document attachments (bilaga)
if (isset($dokumentStatus['dokbilaga']['bilaga']) && is_array($dokumentStatus['dokbilaga']['bilaga'])) {
$this->processBilagor($hangarId, $dokumentStatus['dokbilaga']['bilaga']);
}
// Process document references (referens)
if (isset($dokumentStatus['dokreferens']['referens']) && is_array($dokumentStatus['dokreferens']['referens'])) {
$this->processReferenser($hangarId, $dokumentStatus['dokreferens']['referens']);
}
}
protected function importDokumentData($dokumentData)
{
$documents = isset($dokumentData['dokument']) ? $dokumentData['dokument'] : [];
// Handle both single document and array of documents
if (! is_array($documents) || (isset($documents['hangar_id']) && ! is_numeric(array_keys($documents)[0]))) {
$documents = [$documents];
}
foreach ($documents as $dokData) {
$this->processDokument($dokData);
}
}
protected function processDokument($dokData)
{
// Create main document
$dokument = Dokument::updateOrCreate(
['dok_id' => $dokData['dok_id'] ?? null],
[
'hangar_id' => $dokData['hangar_id'] ?? null,
'rm' => $dokData['rm'] ?? null,
'beteckning' => $dokData['beteckning'] ?? null,
'typ' => $dokData['typ'] ?? null,
'subtyp' => $dokData['subtyp'] ?? null,
'doktyp' => $dokData['doktyp'] ?? null,
'typrubrik' => $dokData['typrubrik'] ?? null,
'dokumentnamn' => $dokData['dokumentnamn'] ?? null,
'debattnamn' => $dokData['debattnamn'] ?? null,
'tempbeteckning' => $dokData['tempbeteckning'] ?? null,
'organ' => $dokData['organ'] ?? null,
'mottagare' => $dokData['mottagare'] ?? null,
'nummer' => $dokData['nummer'] ?? null,
'slutnummer' => $dokData['slutnummer'] ?? null,
'datum' => isset($dokData['datum']) ? $this->parseDate($dokData['datum']) : null,
'systemdatum' => isset($dokData['systemdatum']) ? $this->parseDate($dokData['systemdatum']) : null,
'publicerad' => isset($dokData['publicerad']) ? $this->parseDate($dokData['publicerad']) : null,
'titel' => $dokData['titel'] ?? null,
'subtitel' => $dokData['subtitel'] ?? null,
'status' => $dokData['status'] ?? null,
'htmlformat' => $dokData['htmlformat'] ?? null,
'relaterat_id' => $dokData['relaterat_id'] ?? null,
'source' => $dokData['source'] ?? null,
'sourceid' => $dokData['sourceid'] ?? null,
'dokument_url_text' => $dokData['dokument_url_text'] ?? null,
'dokument_url_html' => $dokData['dokument_url_html'] ?? null,
'dokumentstatus_url_xml' => $dokData['dokumentstatus_url_xml'] ?? null,
'utskottsforslag_url_xml' => $dokData['utskottsforslag_url_xml'] ?? null,
]
);
// Process related data
if (isset($dokData['aktivitet']) && is_array($dokData['aktivitet'])) {
$this->processAktiviteter($dokument->hangar_id, $dokData['aktivitet']);
}
if (isset($dokData['intressent']) && is_array($dokData['intressent'])) {
$this->processIntressenter($dokument->hangar_id, $dokData['intressent']);
}
if (isset($dokData['bilaga']) && is_array($dokData['bilaga'])) {
$this->processBilagor($dokument->hangar_id, $dokData['bilaga']);
}
if (isset($dokData['forslag']) && is_array($dokData['forslag'])) {
$this->processForslag($dokument->hangar_id, $dokData['forslag']);
}
if (isset($dokData['uppgift']) && is_array($dokData['uppgift'])) {
$this->processUppgifter($dokument->hangar_id, $dokData['uppgift']);
}
if (isset($dokData['referens']) && is_array($dokData['referens'])) {
$this->processReferenser($dokument->hangar_id, $dokData['referens']);
}
}
protected function processAktiviteter($hangarId, $aktiviteter)
{
foreach ($aktiviteter as $aktivitet) {
DokAktivitet::updateOrCreate(
[
'hangar_id' => $hangarId,
'kod' => $aktivitet['kod'] ?? null,
'datum' => isset($aktivitet['datum']) ? $this->parseDate($aktivitet['datum']) : null,
],
[
'namn' => $aktivitet['namn'] ?? null,
'status' => $aktivitet['status'] ?? null,
'ordning' => $aktivitet['ordning'] ?? null,
'process' => $aktivitet['process'] ?? null,
]
);
}
}
protected function processIntressenter($hangarId, $intressenter)
{
foreach ($intressenter as $intressent) {
dump($intressent);
DokIntressent::updateOrCreate(
[
'hangar_id' => $hangarId,
'intressent_id' => $intressent['intressent_id'] ?? null,
],
[
'namn' => $intressent['namn'] ?? null,
'partibet' => $intressent['partibet'] ?? null,
'ordning' => $intressent['ordning'] ?? null,
'roll' => $intressent['roll'] ?? null,
]
);
}
}
protected function processBilagor($hangarId, $bilagor)
{
foreach ($bilagor as $bilaga) {
DokBilaga::updateOrCreate(
[
'hangar_id' => $hangarId,
'dok_id' => $bilaga['dok_id'] ?? null,
'filnamn' => $bilaga['filnamn'] ?? null,
],
[
'titel' => $bilaga['titel'] ?? null,
'subtitel' => $bilaga['subtitel'] ?? null,
'filstorlek' => $bilaga['filstorlek'] ?? null,
'filtyp' => $bilaga['filtyp'] ?? null,
'fil_url' => $bilaga['fil_url'] ?? null,
]
);
}
}
protected function processForslag($hangarId, $forslag)
{
foreach ($forslag as $forslagItem) {
DokForslag::updateOrCreate(
[
'hangar_id' => $hangarId,
'nummer' => $forslagItem['nummer'] ?? null,
],
[
'beteckning' => $forslagItem['beteckning'] ?? null,
'lydelse' => $forslagItem['lydelse'] ?? null,
'lydelse2' => $forslagItem['lydelse2'] ?? null,
'utskottet' => $forslagItem['utskottet'] ?? null,
'kammaren' => $forslagItem['kammaren'] ?? null,
'behandlas_i' => $forslagItem['behandlas_i'] ?? null,
'behandlas_i_punkt' => $forslagItem['behandlas_i_punkt'] ?? null,
'kammarbeslutstyp' => $forslagItem['kammarbeslutstyp'] ?? null,
'intressent' => $forslagItem['intressent'] ?? null,
'avsnitt' => $forslagItem['avsnitt'] ?? null,
'grundforfattning' => $forslagItem['grundforfattning'] ?? null,
'andringsforfattning' => $forslagItem['andringsforfattning'] ?? null,
]
);
}
}
protected function processUppgifter($hangarId, $uppgifter)
{
foreach ($uppgifter as $uppgift) {
DokUppgift::updateOrCreate(
[
'hangar_id' => $hangarId,
'kod' => $uppgift['kod'] ?? null,
],
[
'namn' => $uppgift['namn'] ?? null,
'text' => $uppgift['text'] ?? null,
'dok_id' => $uppgift['dok_id'] ?? null,
'systemdatum' => isset($uppgift['systemdatum']) ? $this->parseDate($uppgift['systemdatum']) : null,
]
);
}
}
protected function processReferenser($hangarId, $referenser)
{
foreach ($referenser as $referens) {
DokReferens::updateOrCreate(
[
'hangar_id' => $hangarId,
'referenstyp' => $referens['referenstyp'] ?? null,
'ref_dok_id' => $referens['ref_dok_id'] ?? null,
],
[
'uppgift' => $referens['uppgift'] ?? null,
'ref_dok_typ' => $referens['ref_dok_typ'] ?? null,
'ref_dok_rm' => $referens['ref_dok_rm'] ?? null,
'ref_dok_bet' => $referens['ref_dok_bet'] ?? null,
'ref_dok_titel' => $referens['ref_dok_titel'] ?? null,
'ref_dok_subtitel' => $referens['ref_dok_subtitel'] ?? null,
'ref_dok_subtyp' => $referens['ref_dok_subtyp'] ?? null,
'ref_dok_dokumentnamn' => $referens['ref_dok_dokumentnamn'] ?? null,
]
);
}
}
protected function importPersonData($personData)
{
$persons = isset($personData['person']) ? $personData['person'] : [];
// Handle both single person and array of persons
if (! is_array($persons) || (isset($persons['intressent_id']) && ! is_numeric(array_keys($persons)[0]))) {
$persons = [$persons];
}
foreach ($persons as $personInfo) {
$this->processPerson($personInfo);
}
}
protected function processPerson($personInfo)
{
$person = Person::updateOrCreate(
['intressent_id' => $personInfo['intressent_id'] ?? null],
[
'född_år' => $personInfo['fodd_ar'] ?? $personInfo['född_år'] ?? null,
'kön' => $personInfo['kon'] ?? $personInfo['kön'] ?? null,
'efternamn' => $personInfo['efternamn'] ?? null,
'tilltalsnamn' => $personInfo['tilltalsnamn'] ?? null,
'sorteringsnamn' => $personInfo['sorteringsnamn'] ?? null,
'iort' => $personInfo['iort'] ?? null,
'parti' => $personInfo['parti'] ?? null,
'valkrets' => $personInfo['valkrets'] ?? null,
'status' => $personInfo['status'] ?? null,
]
);
// Process person assignments
if (isset($personInfo['personuppdrag']['uppdrag']) && is_array($personInfo['personuppdrag']['uppdrag'])) {
$this->processPersonUppdrag($person->intressent_id, $personInfo['personuppdrag']['uppdrag']);
}
// Process person information
if (isset($personInfo['personuppgift']['uppgift']) && is_array($personInfo['personuppgift']['uppgift'])) {
$this->processPersonUppgift($person->intressent_id, $personInfo['personuppgift']['uppgift']);
}
}
protected function processPersonUppdrag($intressentId, $uppdragList)
{
foreach ($uppdragList as $uppdrag) {
PersonUppdrag::updateOrCreate(
[
'intressent_id' => $intressentId,
'organ_kod' => $uppdrag['organ_kod'] ?? null,
'roll_kod' => $uppdrag['roll_kod'] ?? null,
'ordningsnummer' => $uppdrag['ordningsnummer'] ?? null,
],
[
'status' => $uppdrag['status'] ?? null,
'typ' => $uppdrag['typ'] ?? null,
'from' => isset($uppdrag['from']) ? $this->parseDate($uppdrag['from']) : null,
'tom' => isset($uppdrag['tom']) ? $this->parseDate($uppdrag['tom']) : null,
'uppgift' => $uppdrag['uppgift'] ?? null,
]
);
}
}
protected function processPersonUppgift($intressentId, $uppgiftList)
{
foreach ($uppgiftList as $uppgift) {
PersonUppgift::updateOrCreate(
[
'intressent_id' => $intressentId,
'uppgift_kod' => $uppgift['kod'] ?? null,
],
[
'uppgift' => $uppgift['uppgift'] ?? null,
'uppgift_typ' => $uppgift['typ'] ?? null,
]
);
}
}
protected function importOrganData($organData)
{
$organs = isset($organData['organ']) ? $organData['organ'] : [];
// Handle both single organ and array of organs
if (! is_array($organs) || (isset($organs['id']) && ! is_numeric(array_keys($organs)[0]))) {
$organs = [$organs];
}
foreach ($organs as $organInfo) {
Organ::updateOrCreate(
['id' => $organInfo['id'] ?? null],
[
'kod' => $organInfo['kod'] ?? null,
'namn' => $organInfo['namn'] ?? null,
'typ' => $organInfo['typ'] ?? null,
'status' => $organInfo['status'] ?? null,
'sortering' => $organInfo['sortering'] ?? null,
'namn_en' => $organInfo['namn_en'] ?? null,
'domän' => $organInfo['doman'] ?? $organInfo['domän'] ?? null,
'beskrivning' => $organInfo['beskrivning'] ?? null,
]
);
}
}
protected function importSingleDokument($dokument)
{
$this->processDokument($dokument);
}
protected function parseDate($dateString)
{
if (empty($dateString)) {
return null;
}
try {
return \Carbon\Carbon::parse($dateString);
} catch (\Exception $e) {
Log::warning("Could not parse date: {$dateString}");
return null;
}
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Livewire\Motion;
use App\Enums\Parties as PartyEnum;
use App\Services\RiksdagenService;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class Search extends Component
{
use WithPagination;
#[Url]
public $query = '';
#[Url]
public $party = '';
#[Url]
public $dateInterval = '';
private $paginator;
public $loading = false;
public function mount()
{
$this->searchMotions();
}
public function updatedQuery()
{
$this->resetPage();
$this->searchMotions();
}
public function updatedParty()
{
$this->resetPage();
$this->searchMotions();
}
public function updatedDateInterval()
{
$this->resetPage();
$this->searchMotions();
}
public function searchMotions()
{
if (empty($this->query) && empty($this->party) && empty($this->dateInterval)) {
$this->paginator = null;
return;
}
$this->loading = true;
$cacheKey = 'motions_'.md5($this->query.$this->party.$this->dateInterval.$this->getPage());
$result = Cache::remember($cacheKey, 60 * 5, function () {
return app(RiksdagenService::class)->searchMotions(
query: $this->query,
party: $this->party,
dateInterval: $this->dateInterval,
page: $this->getPage()
);
});
$motions = $result->original->dokumentlista->dokument ?? [];
$totalResults = (int) ($result->original->dokumentlista->{'@traffar'} ?? 0);
$perPage = 20; // Default items per page from Riksdag API
$currentPage = $this->getPage();
$this->paginator = new LengthAwarePaginator(
items: collect($motions),
total: $totalResults,
perPage: $perPage,
currentPage: $currentPage,
options: [
'path' => request()->url(),
'pageName' => 'page',
]
);
$this->loading = false;
}
#[Computed()]
public function motions()
{
return $this->paginator ? collect($this->paginator->items()) : collect();
}
#[Computed()]
public function totalResults()
{
return $this->paginator ? $this->paginator->total() : 0;
}
public function clearFilters()
{
$this->query = '';
$this->party = '';
$this->dateInterval = '';
$this->resetPage();
$this->searchMotions();
}
#[Computed()]
public function parties()
{
return collect(PartyEnum::cases())->mapWithKeys(function ($party) {
return [$party->value => $party->label()];
});
}
#[Computed()]
public function dateIntervals()
{
return [
'2025/26' => '2025/26',
'2024/25' => '2024/25',
'2023/24' => '2023/24',
'2022/23' => '2022/23',
'2021/22' => '2021/22',
'2020/21' => '2020/21',
'2019/20' => '2019/20',
'2018/19' => '2018/19',
];
}
public function render()
{
$this->searchMotions();
return view('livewire.motion.search', [
'paginatedMotions' => $this->paginator,
])->title('Sök Motioner - Riksdagen App');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Livewire\Motion;
use App\Services\RiksdagenService;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Show extends Component
{
public $motionId;
public $motion;
private RiksdagenService $service;
public function mount($motionId)
{
$this->motionId = $motionId;
$this->service = app(RiksdagenService::class);
$result = Cache::remember('motion_'.$motionId, 24 * 60 * 60, function () use ($motionId) {
return $this->service->getMotion($motionId);
});
$this->motion = $result->original->dokumentlista->dokument[0] ?? null;
}
#[Computed()]
public function riksdagenUrl()
{
return 'https://www.riksdagen.se/sv/dokument-lagar/dokument/'.$this->motionId;
}
#[Computed()]
public function authors()
{
if (! $this->motion || ! isset($this->motion->dokintressent->intressent)) {
return collect();
}
$intressenter = $this->motion->dokintressent->intressent;
if (! is_array($intressenter)) {
$intressenter = [$intressenter];
}
return collect($intressenter)->where('roll', 'undertecknare');
}
#[Computed()]
public function attachments()
{
if (! $this->motion || ! isset($this->motion->filbilaga->fil)) {
return collect();
}
$files = $this->motion->filbilaga->fil;
if (! is_array($files)) {
$files = [$files];
}
return collect($files);
}
public function render()
{
return view('livewire.motion.show')
->title(($this->motion->titel ?? 'Motion').' - Riksdagen App');
}
}

View File

@@ -2,30 +2,35 @@
namespace App\Livewire\Person;
use Livewire\Component;
use App\Services\RiksdagenService;
use App\Enums\Parties;
use App\Services\RiksdagenService;
use Livewire\Component;
class Search extends Component
{
public $firstName = '';
public $lastName = '';
public $party = '';
public $results = [];
public $parties = [];
public function mount()
{
$this->parties = collect(Parties::cases())->sortBy(fn($party) => $party->label())->toArray();
$this->parties = collect(Parties::cases())->sortBy(fn ($party) => $party->label())->toArray();
}
public function search()
{
$service = app(RiksdagenService::class);
$this->results = $service->searchPerson(
firstName: $this->firstName,
lastName: $this->lastName,
party: $this->party
party: $this->party,
)->original;
}

View File

@@ -2,11 +2,12 @@
namespace App\Livewire\Person;
use Livewire\Component;
use App\Services\RiksdagenService;
use Livewire\Attributes\Computed;
use Asantibanez\LivewireCharts\Models\PieChartModel;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Lazy;
use Livewire\Component;
#[Lazy()]
class Show extends Component
@@ -17,9 +18,15 @@ class Show extends Component
public $votes = [];
public $motions = [];
public $votesByYear = [];
public $selectedYear;
public $motionsByYear = [];
public $votesSelectedYear;
public $motionsSelectedYear;
public $selectedUppdragTab = 'current';
@@ -27,47 +34,87 @@ class Show extends Component
public $previousUppdrag = [];
private RiksdagenService $service;
public function mount($personId)
{
$this->personId = $personId;
$service = app(RiksdagenService::class);
$result = $service->searchPerson(mp_id: $personId);
$this->service = app(RiksdagenService::class);
$result = Cache::remember('person_'.$personId, 24 * 60 * 60, function () use ($personId) {
return $this->service->searchPerson(mp_id: $personId);
});
$this->person = $result->original->personlista->person ?? null;
$this->getPersonVotes();
$this->getPersonMotions();
$this->groupUppdrag();
}
#[Computed()]
public function riksdagenUrl()
{
return "https://www.riksdagen.se/sv/ledamoter-partier/ledamot/" . $this->person->tilltalsnamn . "-" . $this->person->efternamn . "_" . $this->personId;
return 'https://www.riksdagen.se/sv/ledamoter-partier/ledamot/'.$this->person->tilltalsnamn.'-'.$this->person->efternamn.'_'.$this->personId;
}
public function getPersonVotes()
{
$service = app(RiksdagenService::class);
$result = $service->searchVotes(mp_id: $this->personId);
$result = Cache::remember('person_votes_'.$this->personId, 24 * 60 * 60, function () {
return $this->service->searchVotes(mp_id: $this->personId);
});
$votesList = $result->original->voteringlista->votering ?? [];
// Group votes by year (from rm field like 2025/26)
// Group votes by "rm" field (e.g., 2024/25)
foreach ($votesList as $vote) {
$year = explode('/', $vote->rm)[0]; // Extract year from rm like "2025/26"
if (!isset($this->votesByYear[$year])) {
$this->votesByYear[$year] = [];
$rm = $vote->rm;
if (! isset($this->votesByYear[$rm])) {
$this->votesByYear[$rm] = [];
}
$this->votesByYear[$year][] = $vote;
$this->votesByYear[$rm][] = $vote;
}
// Set default selected year to the most recent
if (!empty($this->votesByYear)) {
$this->selectedYear = max(array_keys($this->votesByYear));
if (! empty($this->votesByYear)) {
// Sort keys as strings (e.g., 2024/25, 2023/24, ...)
$years = array_keys($this->votesByYear);
rsort($years, SORT_STRING);
$this->votesSelectedYear = $years[0];
}
}
public function selectYear($year)
public function getPersonMotions()
{
$this->selectedYear = $year;
$result = Cache::remember('person_motions_'.$this->personId, 24 * 60 * 60, function () {
return $this->service->getPersonMotions(mp_id: $this->personId);
});
$motionsList = $result->original->dokumentlista->dokument ?? [];
// Group votes by "rm" field (e.g., 2024/25)
foreach ($motionsList as $motion) {
$rm = $motion->rm;
if (! isset($this->motionsByYear[$rm])) {
$this->motionsByYear[$rm] = [];
}
$this->motionsByYear[$rm][] = $motion;
}
// Set default selected year to the most recent
if (! empty($this->motionsByYear)) {
// Sort keys as strings (e.g., 2024/25, 2023/24, ...)
$years = array_keys($this->motionsByYear);
rsort($years, SORT_STRING);
$this->motionsSelectedYear = $years[0];
}
}
public function selectVotesYear($year)
{
$this->votesSelectedYear = $year;
}
public function selectMotionsYear($year)
{
$this->motionsSelectedYear = $year;
}
public function selectUppdragTab($tab)
@@ -77,7 +124,7 @@ class Show extends Component
public function groupUppdrag()
{
if (!$this->person || !isset($this->person->personuppdrag->uppdrag)) {
if (! $this->person || ! isset($this->person->personuppdrag->uppdrag)) {
return;
}
@@ -98,11 +145,11 @@ class Show extends Component
}
// Sort by date (most recent first)
usort($this->currentUppdrag, function($a, $b) {
usort($this->currentUppdrag, function ($a, $b) {
return \Carbon\Carbon::parse($b->from)->timestamp - \Carbon\Carbon::parse($a->from)->timestamp;
});
usort($this->previousUppdrag, function($a, $b) {
usort($this->previousUppdrag, function ($a, $b) {
return \Carbon\Carbon::parse($b->tom ?: $b->from)->timestamp - \Carbon\Carbon::parse($a->tom ?: $a->from)->timestamp;
});
}
@@ -110,16 +157,16 @@ class Show extends Component
#[Computed()]
public function votingStatistics()
{
if (!$this->selectedYear || !isset($this->votesByYear[$this->selectedYear])) {
if (! $this->votesSelectedYear || ! isset($this->votesByYear[$this->votesSelectedYear])) {
return [];
}
$votes = $this->votesByYear[$this->selectedYear];
$votes = $this->votesByYear[$this->votesSelectedYear];
$statistics = [];
foreach ($votes as $vote) {
$voteType = $vote->rost;
if (!isset($statistics[$voteType])) {
if (! isset($statistics[$voteType])) {
$statistics[$voteType] = 0;
}
$statistics[$voteType]++;
@@ -133,8 +180,8 @@ class Show extends Component
{
$statistics = $this->votingStatistics;
$pieChart = (new PieChartModel())
->setTitle('Voteringsstatistik för ' . $this->selectedYear)
$pieChart = (new PieChartModel)
->setTitle('Voteringsstatistik för '.$this->votesSelectedYear)
->setAnimated(true)
->withDataLabels();

20
app/Models/Anforande.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Anforande extends Model
{
protected $table = 'anforande';
public $timestamps = false;
public function dokument()
{
return $this->belongsTo(Dokument::class, 'dok_hangar_id', 'hangar_id');
}
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
}

20
app/Models/Debatt.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Debatt extends Model
{
protected $table = 'debatt';
public $timestamps = false;
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokAktivitet extends Model
{
protected $table = 'dokaktivitet';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'kod',
'datum',
'namn',
'status',
'ordning',
'process',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

28
app/Models/DokBilaga.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokBilaga extends Model
{
protected $table = 'dokbilaga';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'dok_id',
'filnamn',
'titel',
'subtitel',
'filstorlek',
'filtyp',
'fil_url',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

34
app/Models/DokForslag.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokForslag extends Model
{
protected $table = 'dokforslag';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'nummer',
'beteckning',
'lydelse',
'lydelse2',
'utskottet',
'kammaren',
'behandlas_i',
'behandlas_i_punkt',
'kammarbeslutstyp',
'intressent',
'avsnitt',
'grundforfattning',
'andringsforfattning',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokIntressent extends Model
{
protected $table = 'dokintressent';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'intressent_id',
'namn',
'partibet',
'ordning',
'roll',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokMotforslag extends Model
{
protected $table = 'dokmotforslag';
public $timestamps = false;
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokReferens extends Model
{
protected $table = 'dokreferens';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'referenstyp',
'ref_dok_id',
'uppgift',
'ref_dok_typ',
'ref_dok_rm',
'ref_dok_bet',
'ref_dok_titel',
'ref_dok_subtitel',
'ref_dok_subtyp',
'ref_dok_dokumentnamn',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

30
app/Models/DokUppgift.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokUppgift extends Model
{
protected $table = 'dokuppgift';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'kod',
'namn',
'text',
'dok_id',
'systemdatum',
];
protected $casts = [
'systemdatum' => 'datetime',
];
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DokUtskottsforslag extends Model
{
protected $table = 'dokutskottsforslag';
public $timestamps = false;
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
}

102
app/Models/Dokument.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Dokument extends Model
{
protected $table = 'dokument';
protected $primaryKey = 'hangar_id';
public $timestamps = false;
protected $fillable = [
'hangar_id',
'dok_id',
'rm',
'beteckning',
'typ',
'subtyp',
'doktyp',
'typrubrik',
'dokumentnamn',
'debattnamn',
'tempbeteckning',
'organ',
'mottagare',
'nummer',
'slutnummer',
'datum',
'systemdatum',
'publicerad',
'titel',
'subtitel',
'status',
'htmlformat',
'relaterat_id',
'source',
'sourceid',
'dokument_url_text',
'dokument_url_html',
'dokumentstatus_url_xml',
'utskottsforslag_url_xml',
'html',
];
public function utskottsforslag()
{
return $this->hasMany(DokUtskottsforslag::class, 'hangar_id', 'hangar_id');
}
public function motforslag()
{
return $this->hasMany(DokMotforslag::class, 'hangar_id', 'hangar_id');
}
public function aktiviteter()
{
return $this->hasMany(DokAktivitet::class, 'hangar_id', 'hangar_id');
}
public function intressenter()
{
return $this->hasMany(DokIntressent::class, 'hangar_id', 'hangar_id');
}
public function forslag()
{
return $this->hasMany(DokForslag::class, 'hangar_id', 'hangar_id');
}
public function uppgifter()
{
return $this->hasMany(DokUppgift::class, 'hangar_id', 'hangar_id');
}
public function bilagor()
{
return $this->hasMany(DokBilaga::class, 'hangar_id', 'hangar_id');
}
public function referenser()
{
return $this->hasMany(DokReferens::class, 'hangar_id', 'hangar_id');
}
public function debatter()
{
return $this->hasMany(Debatt::class, 'hangar_id', 'hangar_id');
}
public function voteringar()
{
return $this->hasMany(Votering::class, 'hangar_id', 'hangar_id');
}
public function anforanden()
{
return $this->hasMany(Anforande::class, 'dok_hangar_id', 'hangar_id');
}
}

31
app/Models/Organ.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Organ extends Model
{
protected $table = 'organ';
protected $primaryKey = 'id';
public $timestamps = false;
protected $fillable = [
'id',
'kod',
'namn',
'typ',
'status',
'sortering',
'namn_en',
'domän',
'beskrivning',
];
public function personUppdrag()
{
return $this->hasMany(PersonUppdrag::class, 'organ_kod', 'kod');
}
}

61
app/Models/Person.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Person extends Model
{
protected $table = 'person';
protected $primaryKey = 'intressent_id';
protected $keyType = 'string';
public $incrementing = false;
public $timestamps = false;
protected $fillable = [
'intressent_id',
'född_år',
'kön',
'efternamn',
'tilltalsnamn',
'sorteringsnamn',
'iort',
'parti',
'valkrets',
'status',
];
public function uppdrag()
{
return $this->hasMany(PersonUppdrag::class, 'intressent_id', 'intressent_id');
}
public function uppgifter()
{
return $this->hasMany(PersonUppgift::class, 'intressent_id', 'intressent_id');
}
public function voteringar()
{
return $this->hasMany(Votering::class, 'intressent_id', 'intressent_id');
}
public function anforanden()
{
return $this->hasMany(Anforande::class, 'intressent_id', 'intressent_id');
}
public function debatter()
{
return $this->hasMany(Debatt::class, 'intressent_id', 'intressent_id');
}
public function dokIntressenter()
{
return $this->hasMany(DokIntressent::class, 'intressent_id', 'intressent_id');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PersonUppdrag extends Model
{
protected $table = 'personuppdrag';
public $timestamps = false;
protected $fillable = [
'intressent_id',
'organ_kod',
'roll_kod',
'ordningsnummer',
'status',
'typ',
'from',
'tom',
'uppgift',
];
protected $casts = [
'from' => 'datetime',
'tom' => 'datetime',
];
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
public function organ()
{
return $this->belongsTo(Organ::class, 'organ_kod', 'kod');
}
public function roll()
{
return $this->belongsTo(Roll::class, 'roll_kod', 'kod');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PersonUppgift extends Model
{
protected $table = 'personuppgift';
public $timestamps = false;
protected $fillable = [
'intressent_id',
'uppgift_kod',
'uppgift',
'uppgift_typ',
];
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
}

31
app/Models/Planering.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Planering extends Model
{
protected $table = 'planering';
protected $primaryKey = 'nyckel';
public $timestamps = false;
protected $casts = [
'datum' => 'datetime',
'publicerad' => 'datetime',
'uppdaterad' => 'datetime',
'wn_expires' => 'datetime',
'timestamp' => 'datetime',
'slutdatum' => 'datetime',
'webbtvlive' => 'boolean',
];
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
public function mottagare()
{
return $this->belongsTo(Person::class, 'mottagare_id', 'intressent_id');
}
}

16
app/Models/Riksmote.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Riksmote extends Model
{
protected $table = 'riksmote';
protected $primaryKey = 'pk';
public $timestamps = false;
protected $casts = [
'start' => 'datetime',
'slut' => 'datetime',
];
}

16
app/Models/Roll.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Roll extends Model
{
protected $table = 'roll';
protected $primaryKey = 'pk';
public $timestamps = false;
public function personUppdrag()
{
return $this->hasMany(PersonUppdrag::class, 'roll_kod', 'kod');
}
}

20
app/Models/Votering.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Votering extends Model
{
protected $table = 'votering';
public $timestamps = false;
public function dokument()
{
return $this->belongsTo(Dokument::class, 'hangar_id', 'hangar_id');
}
public function person()
{
return $this->belongsTo(Person::class, 'intressent_id', 'intressent_id');
}
}

View File

@@ -22,7 +22,7 @@ class RiksdagenService
string $lastName = '',
string $party = ''
): JsonResponse {
$response = $this->http->get('personlista/', [
$response = $this->http->get('personlista/', [
'query' => [
'iid' => $mp_id,
'parti' => $party,
@@ -30,8 +30,8 @@ class RiksdagenService
'enamn' => $lastName,
'utformat' => 'json',
'sort' => 'sorteringsnamn',
'sortorder' => 'asc'
]
'sortorder' => 'asc',
],
])->getBody()->getContents();
$data = json_decode($response);
@@ -44,14 +44,92 @@ class RiksdagenService
string $party = '',
string $date = '',
): JsonResponse {
$response = $this->http->get('voteringlista/', [
$response = $this->http->get('voteringlista/', [
'query' => [
'iid' => $mp_id,
'rm' => $date,
'parti' => $party,
'utformat' => 'json',
'sz' => '500',
]
'sz' => '3000',
],
])->getBody()->getContents();
$data = json_decode($response);
return response()->json($data);
}
public function getPersonMotions(
string $mp_id,
string $date = '',
): JsonResponse {
$response = $this->http->get('dokumentlista/', [
'query' => [
'iid' => $mp_id,
'rm' => $date,
'doktyp' => 'mot',
'utformat' => 'json',
],
])->getBody()->getContents();
$data = json_decode($response);
return response()->json($data);
}
public function getPartyMotions(
string $party,
string $date = '',
): JsonResponse {
$response = $this->http->get('dokumentlista/', [
'query' => [
'parti' => $party,
'rm' => $date,
'doktyp' => 'mot',
'utformat' => 'json',
],
])->getBody()->getContents();
$data = json_decode($response);
return response()->json($data);
}
public function searchMotions(
string $query = '',
string $party = '',
string $dateInterval = '',
int $page = 1
): JsonResponse {
$response = $this->http->get('dokumentlista/', [
'query' => array_filter([
'sok' => $query,
'parti' => $party,
'rm' => $dateInterval,
'doktyp' => 'mot',
'utformat' => 'json',
'p' => $page,
]),
])->getBody()->getContents();
$data = json_decode($response);
return response()->json($data);
}
public function getMotion(
string $bet,
string $tempbet,
string $nr,
): JsonResponse {
$response = $this->http->get('dokumentlista/', [
'query' => [
'bet' => $bet,
'tempbet' => $tempbet,
'nr' => $nr,
'utformat' => 'json',
],
])->getBody()->getContents();
$data = json_decode($response);