diff --git a/app/Http/Controllers/Api/V1/NomenclatureController.php b/app/Http/Controllers/Api/V1/NomenclatureController.php index a6e32ec..5e9c815 100644 --- a/app/Http/Controllers/Api/V1/NomenclatureController.php +++ b/app/Http/Controllers/Api/V1/NomenclatureController.php @@ -16,7 +16,7 @@ class NomenclatureController extends Controller { /** - * @operationId Elections + * @operationId Nomenclature/Elections */ public function elections(): JsonResource { @@ -29,7 +29,7 @@ public function elections(): JsonResource } /** - * @operationId Countries + * @operationId Nomenclature/Countries */ public function countries(): JsonResource { @@ -37,7 +37,7 @@ public function countries(): JsonResource } /** - * @operationId Counties + * @operationId Nomenclature/Counties */ public function counties(): JsonResource { @@ -45,7 +45,7 @@ public function counties(): JsonResource } /** - * @operationId County + * @operationId Nomenclature/County */ public function county(County $county): JsonResource { diff --git a/app/Http/Controllers/Api/V1/ResultsController.php b/app/Http/Controllers/Api/V1/ResultsController.php new file mode 100644 index 0000000..4d52d38 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ResultsController.php @@ -0,0 +1,185 @@ +votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::TOTAL, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = DataLevel::TOTAL->getLabel(); + + return ResultResource::make($result); + } + + /** + * @operationId Results/Diaspora + */ + public function diaspora(Election $election): JsonResource + { + $result = RecordsRepository::getForLevel( + election: $election, + level: DataLevel::DIASPORA, + aggregate: true, + toBase: true, + ); + + $result->votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::DIASPORA, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = DataLevel::DIASPORA->getLabel(); + + return ResultResource::make($result); + } + + /** + * @operationId Results/Diaspora/Country + */ + public function country(Election $election, Country $country): JsonResource + { + $result = RecordsRepository::getForLevel( + election: $election, + level: DataLevel::DIASPORA, + country: $country->id, + aggregate: true, + toBase: true, + ); + + $result->votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::DIASPORA, + country: $country->id, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = $country->name; + + return ResultResource::make($result); + } + + /** + * @operationId Results/National + */ + public function national(Election $election): JsonResource + { + $result = RecordsRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + aggregate: true, + toBase: true, + ); + + $result->votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = DataLevel::NATIONAL->getLabel(); + + return ResultResource::make($result); + } + + /** + * @operationId Results/National/County + */ + public function county(Election $election, County $county): JsonResource + { + $result = RecordsRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + county: $county->id, + aggregate: true, + toBase: true, + ); + + $result->votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + county: $county->id, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = $county->name; + + return ResultResource::make($result); + } + + /** + * @operationId Results/National/County/Locality + */ + public function locality(Election $election, County $county, Locality $locality): JsonResource + { + abort_unless($locality->county_id === $county->id, 404); + + $result = RecordsRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + county: $county->id, + locality: $locality->id, + aggregate: true, + toBase: true, + ); + + $result->votes = VotesRepository::getForLevel( + election: $election, + level: DataLevel::NATIONAL, + county: $county->id, + locality: $locality->id, + aggregate: true, + toBase: true, + ); + + $result->last_updated_at = $election->records_updated_at; + + $result->name = "{$locality->name}, {$county->name}"; + + return ResultResource::make($result); + } +} diff --git a/app/Http/Controllers/Api/V1/TurnoutController.php b/app/Http/Controllers/Api/V1/TurnoutController.php index 8db3ca3..5be0639 100644 --- a/app/Http/Controllers/Api/V1/TurnoutController.php +++ b/app/Http/Controllers/Api/V1/TurnoutController.php @@ -21,9 +21,9 @@ class TurnoutController extends Controller { /** - * @operationId Total + * @operationId Turnout/Total */ - public function total(Election $election)//: JsonResource + public function total(Election $election): JsonResource { $result = TurnoutRepository::getForLevel( election: $election, @@ -52,7 +52,7 @@ public function total(Election $election)//: JsonResource } /** - * @operationId Diaspora + * @operationId Turnout/Diaspora */ public function diaspora(Election $election): JsonResource { @@ -110,7 +110,7 @@ public function diaspora(Election $election): JsonResource } /** - * @operationId DiasporaCountry + * @operationId Turnout/Diaspora/Country */ public function country(Election $election, Country $country): JsonResource { @@ -145,7 +145,7 @@ public function country(Election $election, Country $country): JsonResource } /** - * @operationId National + * @operationId Turnout/National */ public function national(Election $election): JsonResource { @@ -201,7 +201,7 @@ public function national(Election $election): JsonResource } /** - * @operationId NationalCounty + * @operationId Turnout/National/County */ public function county(Election $election, County $county): JsonResource { diff --git a/app/Http/Resources/Result/CandidatesResource.php b/app/Http/Resources/Result/CandidatesResource.php new file mode 100644 index 0000000..fd8a579 --- /dev/null +++ b/app/Http/Resources/Result/CandidatesResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + /* + * @var string $name + * Name of the candidate + */ + 'name' => $this['name'], + /* + * @var int $votes + * Number of votes the candidate received + */ + 'votes' => (int) $this['votes'], + + ]; + } +} diff --git a/app/Http/Resources/Result/ResultResource.php b/app/Http/Resources/Result/ResultResource.php new file mode 100644 index 0000000..49b0373 --- /dev/null +++ b/app/Http/Resources/Result/ResultResource.php @@ -0,0 +1,57 @@ + + */ + public function toArray(Request $request): array + { + return [ + + /* + * @var \DateTime $last_updated_at + */ + 'last_updated_at' => data_get($this->resource, 'last_updated_at')?->toDateTimeString(), + + /* + * @var string $name + */ + + 'name' => $this->name, + + /* + * @var int $eligible_voters_total + */ + 'eligible_voters_total' => (int) $this->eligible_voters_total, + + /* + * @var int $present_voters_total + */ + 'present_voters_total' => (int) $this->present_voters_total, + + /* + * @var int $votes_valid + * Number of valid votes + */ + 'votes_valid' => (int) $this->votes_valid, + + /* + * @var int $votes_null + * Number of null votes + */ + 'votes_null' => (int) $this->votes_null, + + 'candidates' => CandidatesResource::collection($this->votes), + ]; + } +} diff --git a/app/Jobs/Parlamentare06122020/Records/FetchRecordsJob.php b/app/Jobs/Parlamentare06122020/Records/FetchRecordsJob.php index da1d405..43ea715 100644 --- a/app/Jobs/Parlamentare06122020/Records/FetchRecordsJob.php +++ b/app/Jobs/Parlamentare06122020/Records/FetchRecordsJob.php @@ -57,7 +57,6 @@ public function execute(): void ); }); - $electionName = $this->scheduledJob->election->getFilamentName(); $electionId = $this->scheduledJob->election_id; diff --git a/app/Jobs/Parlamentare06122020/Records/ImportCountyRecordsJob.php b/app/Jobs/Parlamentare06122020/Records/ImportCountyRecordsJob.php index 6023071..74760a4 100644 --- a/app/Jobs/Parlamentare06122020/Records/ImportCountyRecordsJob.php +++ b/app/Jobs/Parlamentare06122020/Records/ImportCountyRecordsJob.php @@ -68,7 +68,7 @@ public function handle(): void 'section' => $row['precinct_nr'], 'part' => $part, - 'eligible_voters_permanent' => $row ['a'], + 'eligible_voters_permanent' => $row['a'], 'eligible_voters_special' => 0, 'present_voters_permanent' => $row['b1'], diff --git a/app/Jobs/Presidential241124/Records/FetchRecordsJob.php b/app/Jobs/Presidential241124/Records/FetchRecordsJob.php index 44972dd..f10e06e 100644 --- a/app/Jobs/Presidential241124/Records/FetchRecordsJob.php +++ b/app/Jobs/Presidential241124/Records/FetchRecordsJob.php @@ -57,7 +57,6 @@ public function execute(): void ); }); - $electionName = $this->scheduledJob->election->getFilamentName(); $electionId = $this->scheduledJob->election_id; diff --git a/app/Jobs/ReferendumBucuresti241124/Records/ImportRecordsJob.php b/app/Jobs/ReferendumBucuresti241124/Records/ImportRecordsJob.php index 08e753a..8b326ef 100644 --- a/app/Jobs/ReferendumBucuresti241124/Records/ImportRecordsJob.php +++ b/app/Jobs/ReferendumBucuresti241124/Records/ImportRecordsJob.php @@ -29,10 +29,10 @@ class ImportRecordsJob implements ShouldQueue public ScheduledJob $scheduledJob; public County $county; - private string $filename; + private string $filename; - public function __construct(ScheduledJob $scheduledJob, $countyCode, $filename) + public function __construct(ScheduledJob $scheduledJob, $countyCode, $filename) { $this->scheduledJob = $scheduledJob; $this->county = County::where('code', $countyCode)->first(); @@ -41,7 +41,7 @@ public function __construct(ScheduledJob $scheduledJob, $countyCode, $filename) public function handle(): void { - $disk = $this->scheduledJob->disk();; + $disk = $this->scheduledJob->disk(); $path = $this->scheduledJob->getSourcePath("{$this->filename}.csv"); if (! $disk->exists($path)) { @@ -76,7 +76,7 @@ public function handle(): void 'present_voters_supliment' => 0, 'present_voters_mail' => 0, //$row['b4'], - 'votes_valid' => (int)$row['a5'] + (int)$row['a6'], + 'votes_valid' => (int) $row['a5'] + (int) $row['a6'], 'votes_null' => $row['a7'], 'papers_received' => $row['a3'], diff --git a/app/Livewire/Pages/ElectionResults.php b/app/Livewire/Pages/ElectionResults.php index c191acf..bf62a7b 100644 --- a/app/Livewire/Pages/ElectionResults.php +++ b/app/Livewire/Pages/ElectionResults.php @@ -4,19 +4,13 @@ namespace App\Livewire\Pages; -use App\Models\Candidate; -use App\Models\Party; -use App\Models\Vote; use App\Repositories\RecordsRepository; use App\Repositories\VotesRepository; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Number; use Illuminate\View\View; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; -use stdClass; class ElectionResults extends ElectionPage { @@ -30,38 +24,10 @@ public function render(): View return view('livewire.pages.election-results'); } - #[Computed] - public function parties(): Collection - { - return Cache::remember("parties:{$this->election->id}", now()->addDay(), function () { - return Party::query() - ->whereBelongsTo($this->election) - // ->whereHas('votes', function (Builder $query) { - // $query->whereBelongsTo($this->election); - // }) - ->with('media') - ->get(); - }); - } - - #[Computed] - public function candidates(): Collection - { - return Cache::remember("candidates:{$this->election->id}", now()->addDay(), function () { - return Candidate::query() - ->whereBelongsTo($this->election) - // ->whereHas('votes', function (Builder $query) { - // $query->whereBelongsTo($this->election); - // }) - ->with('media') - ->get(); - }); - } - #[Computed] public function aggregate(): Collection { - $result = VotesRepository::getForLevel( + return VotesRepository::getForLevel( election: $this->election, level: $this->level, country: $this->country, @@ -69,20 +35,6 @@ public function aggregate(): Collection locality: $this->locality, aggregate: true, ); - - $total = $result->sum('votes'); - - return $result->map(function (Vote $vote) use ($total) { - $votable = $this->getVotable($vote->votable_type, $vote->votable_id); - - return [ - 'name' => $votable->acronym ?? $votable->name, - 'image' => $votable->getFirstMediaUrl(), - 'votes' => ensureNumeric($vote->votes), - 'percent' => percent($vote->votes, $total), - 'color' => hex2rgb($votable->color ?? $this->fallbackColor), - ]; - }); } #[Computed] @@ -115,25 +67,7 @@ public function data(): Collection country: null, county: $this->county, locality: null, - )->mapWithKeys(function (stdClass $vote) { - $votable = $this->getVotable($vote->votable_type, $vote->votable_id); - - return [ - $vote->place => [ - 'value' => percent($vote->votes, $vote->total_votes, formatted: true), - 'color' => $votable->color, - 'label' => $votable->getDisplayName(), - ], - ]; - }); - } - - protected function getVotable(string $type, int $id): Party|Candidate - { - return match ($type) { - (new Party)->getMorphClass() => $this->parties->find($id), - (new Candidate)->getMorphClass() => $this->candidates->find($id), - }; + ); } public function getEmbedUrl(): ?string diff --git a/app/Repositories/VotesRepository.php b/app/Repositories/VotesRepository.php index b53a4b2..c51d8c9 100644 --- a/app/Repositories/VotesRepository.php +++ b/app/Repositories/VotesRepository.php @@ -5,11 +5,15 @@ namespace App\Repositories; use App\Enums\DataLevel; +use App\Models\Candidate; use App\Models\Election; +use App\Models\Party; use App\Models\Vote; use App\Services\CacheService; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use stdClass; use Tpetry\QueryExpressions\Function\Aggregate\Max; use Tpetry\QueryExpressions\Function\Aggregate\Min; use Tpetry\QueryExpressions\Function\Aggregate\Sum; @@ -34,7 +38,7 @@ public static function getForLevel( ) { return CacheService::make('votes', $election, $level, $country, $county, $locality, $aggregate, $toBase, $addSelect) ->remember(function () use ($election, $level, $country, $county, $locality, $aggregate, $toBase, $addSelect) { - $query = Vote::query() + $result = Vote::query() ->whereBelongsTo($election) ->forLevel( level: $level, @@ -44,9 +48,24 @@ public static function getForLevel( aggregate: $aggregate, ) ->when($addSelect, fn (EloquentBuilder $query) => $query->addSelect($addSelect)) - ->when($toBase, fn (EloquentBuilder $query) => $query->toBase()); + ->when($toBase, fn (EloquentBuilder $query) => $query->toBase()) + ->get(); - return $query->get(); + $votables = self::getVotables($result, $election); + + $total = $result->sum('votes'); + + return $result->map(function (stdClass|Vote $vote) use ($votables, $total) { + $votable = $votables->get($vote->votable_type)->get($vote->votable_id); + + return [ + 'name' => $votable->acronym ?? $votable->name, + 'image' => $votable->getFirstMediaUrl(), + 'votes' => (int) ensureNumeric($vote->votes), + 'percent' => percent($vote->votes, $total), + 'color' => hex2rgb($votable->color ?? '#DDD'), + ]; + }); }); } @@ -62,7 +81,7 @@ public static function getMapDataForLevel( ) { return CacheService::make(['votes', 'map-data'], $election, $level, $country, $county, $locality, $aggregate, $toBase, $addSelect) ->remember(function () use ($election, $level, $country, $county, $locality, $aggregate, $toBase, $addSelect) { - $query = DB::query() + $result = DB::query() ->select([ new Alias(DB::raw('ANY_VALUE(votable_id)'), 'votable_id'), new Alias(DB::raw('ANY_VALUE(votable_type)'), 'votable_type'), @@ -86,9 +105,42 @@ public static function getMapDataForLevel( 'votes' ) ->when($addSelect, fn (EloquentBuilder $query) => $query->addSelect($addSelect)) - ->when($toBase, fn (EloquentBuilder $query) => $query->toBase()); + ->when($toBase, fn (EloquentBuilder $query) => $query->toBase()) + ->get(); - return $query->get(); + $votables = self::getVotables($result, $election); + + return $result->mapWithKeys(function (stdClass|Vote $vote) use ($votables) { + $votable = $votables->get($vote->votable_type)->get($vote->votable_id); + + return [ + $vote->place => [ + 'value' => percent($vote->votes, $vote->total_votes, formatted: true), + 'color' => $votable->color, + 'label' => $votable->getDisplayName(), + ], + ]; + }); + }); + } + + protected static function getVotables(Collection $result, Election $election): Collection + { + return $result + ->groupBy('votable_type') + ->map(function (Collection $items, string $type) use ($election) { + $query = match ($type) { + (new Party)->getMorphClass() => Party::query(), + (new Candidate)->getMorphClass() => Candidate::query() + ->with('party.media'), + }; + + return $query + ->whereBelongsTo($election) + ->with('media') + ->whereIn('id', $items->pluck('votable_id')) + ->get() + ->keyBy('id'); }); } } diff --git a/routes/api.php b/routes/api.php index 980132c..9a332a8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Http\Controllers\Api\V1\NomenclatureController; +use App\Http\Controllers\Api\V1\ResultsController; use App\Http\Controllers\Api\V1\TurnoutController; use Illuminate\Support\Facades\Route; @@ -38,5 +39,20 @@ Route::get('/national', 'national')->name('national'); Route::get('/national/{county}', 'county')->name('national.county'); }); + + Route::group([ + 'as' => 'result.', + 'prefix' => 'result', + 'controller' => ResultsController::class, + ], function () { + Route::get('/', 'total')->name('total'); + + Route::get('/diaspora', 'diaspora')->name('diaspora'); + Route::get('/diaspora/{country}', 'country')->name('diaspora.country'); + + Route::get('/national', 'national')->name('national'); + Route::get('/national/{county}', 'county')->name('national.county'); + Route::get('/national/{county}/{locality}', 'locality')->name('national.locality'); + }); }); });