Add nightly town sync from ArcGIS API with autocomplete
All checks were successful
linter / quality (pull_request) Successful in 1m21s
security / Dependency Audit (pull_request) Successful in 1m25s
security / Static Analysis (pull_request) Successful in 1m49s
tests / ci (8.4) (pull_request) Successful in 1m23s
tests / ci (8.5) (pull_request) Successful in 1m27s
All checks were successful
linter / quality (pull_request) Successful in 1m21s
security / Dependency Audit (pull_request) Successful in 1m25s
security / Static Analysis (pull_request) Successful in 1m49s
tests / ci (8.4) (pull_request) Successful in 1m23s
tests / ci (8.5) (pull_request) Successful in 1m27s
- Add towns table with town_pid, town_name, state, population, town_class, date_retired - Add AustralianState enum with label and abbreviation helpers - Add Town model with active() and search() scopes - Add SyncTowns job that paginates ArcGIS API and upserts all 1977 towns - Schedule SyncTowns to run nightly at 02:00 - Add /towns/search endpoint returning JSON suggestions filtered by name and state - Add Alpine-powered autocomplete on origin/destination fields in create form - Add state filter dropdown in journeys card header to narrow autocomplete results
This commit is contained in:
43
app/Enums/AustralianState.php
Normal file
43
app/Enums/AustralianState.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum AustralianState: int
|
||||
{
|
||||
case NSW = 1;
|
||||
case VIC = 2;
|
||||
case QLD = 3;
|
||||
case SA = 4;
|
||||
case WA = 5;
|
||||
case TAS = 6;
|
||||
case NT = 7;
|
||||
case ACT = 8;
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NSW => 'New South Wales',
|
||||
self::VIC => 'Victoria',
|
||||
self::QLD => 'Queensland',
|
||||
self::SA => 'South Australia',
|
||||
self::WA => 'Western Australia',
|
||||
self::TAS => 'Tasmania',
|
||||
self::NT => 'Northern Territory',
|
||||
self::ACT => 'Australian Capital Territory',
|
||||
};
|
||||
}
|
||||
|
||||
public function abbreviation(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NSW => 'NSW',
|
||||
self::VIC => 'VIC',
|
||||
self::QLD => 'QLD',
|
||||
self::SA => 'SA',
|
||||
self::WA => 'WA',
|
||||
self::TAS => 'TAS',
|
||||
self::NT => 'NT',
|
||||
self::ACT => 'ACT',
|
||||
};
|
||||
}
|
||||
}
|
||||
76
app/Jobs/SyncTowns.php
Normal file
76
app/Jobs/SyncTowns.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Town;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncTowns implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private const API_URL = 'https://services-ap1.arcgis.com/ypkPEy1AmwPKGNNv/arcgis/rest/services/Town_Point/FeatureServer/0/query';
|
||||
|
||||
private const PAGE_SIZE = 1000;
|
||||
|
||||
public int $timeout = 300;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$offset = 0;
|
||||
$synced = 0;
|
||||
|
||||
do {
|
||||
$response = Http::timeout(30)->get(self::API_URL, [
|
||||
'outFields' => 'town_pid,town_name,state,population,town_class,date_retired',
|
||||
'where' => '1=1',
|
||||
'f' => 'json',
|
||||
'resultOffset' => $offset,
|
||||
'resultRecordCount' => self::PAGE_SIZE,
|
||||
'orderByFields' => 'objectid ASC',
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('SyncTowns: API request failed', ['offset' => $offset, 'status' => $response->status()]);
|
||||
break;
|
||||
}
|
||||
|
||||
$features = $response->json('features', []);
|
||||
|
||||
if (empty($features)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$records = collect($features)->map(fn (array $feature) => [
|
||||
'town_pid' => $feature['attributes']['town_pid'],
|
||||
'town_name' => $feature['attributes']['town_name'],
|
||||
'state' => $feature['attributes']['state'],
|
||||
'population' => $feature['attributes']['population'],
|
||||
'town_class' => $feature['attributes']['town_class'],
|
||||
'date_retired' => isset($feature['attributes']['date_retired'])
|
||||
? Carbon::createFromTimestampMs($feature['attributes']['date_retired'])
|
||||
: null,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
])->toArray();
|
||||
|
||||
Town::upsert($records, uniqueBy: ['town_pid'], update: [
|
||||
'town_name',
|
||||
'state',
|
||||
'population',
|
||||
'town_class',
|
||||
'date_retired',
|
||||
'updated_at',
|
||||
]);
|
||||
|
||||
$synced += count($features);
|
||||
$offset += self::PAGE_SIZE;
|
||||
} while (count($features) === self::PAGE_SIZE);
|
||||
|
||||
Log::info("SyncTowns: synced {$synced} towns.");
|
||||
}
|
||||
}
|
||||
41
app/Models/Town.php
Normal file
41
app/Models/Town.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\AustralianState;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Town extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\TownFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'town_pid',
|
||||
'town_name',
|
||||
'state',
|
||||
'population',
|
||||
'town_class',
|
||||
'date_retired',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'state' => AustralianState::class,
|
||||
'date_retired' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): void
|
||||
{
|
||||
$query->whereNull('date_retired');
|
||||
}
|
||||
|
||||
public function scopeSearch(Builder $query, string $term): void
|
||||
{
|
||||
$query->where('town_name', 'like', $term.'%');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user