diff --git a/app/Enums/AustralianState.php b/app/Enums/AustralianState.php new file mode 100644 index 0000000..d931a98 --- /dev/null +++ b/app/Enums/AustralianState.php @@ -0,0 +1,43 @@ + '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', + }; + } +} diff --git a/app/Jobs/SyncTowns.php b/app/Jobs/SyncTowns.php new file mode 100644 index 0000000..1df9e6f --- /dev/null +++ b/app/Jobs/SyncTowns.php @@ -0,0 +1,76 @@ +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."); + } +} diff --git a/app/Models/Town.php b/app/Models/Town.php new file mode 100644 index 0000000..2f11d3c --- /dev/null +++ b/app/Models/Town.php @@ -0,0 +1,41 @@ + */ + 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.'%'); + } +} diff --git a/database/factories/TownFactory.php b/database/factories/TownFactory.php new file mode 100644 index 0000000..9d8732a --- /dev/null +++ b/database/factories/TownFactory.php @@ -0,0 +1,23 @@ + + */ +class TownFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2026_03_06_041215_create_towns_table.php b/database/migrations/2026_03_06_041215_create_towns_table.php new file mode 100644 index 0000000..07d7457 --- /dev/null +++ b/database/migrations/2026_03_06_041215_create_towns_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('town_pid', 15)->unique(); + $table->string('town_name', 50); + $table->unsignedTinyInteger('state'); + $table->unsignedInteger('population')->nullable(); + $table->unsignedTinyInteger('town_class')->nullable(); + $table->timestamp('date_retired')->nullable(); + $table->timestamps(); + + $table->index(['town_name', 'state']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('towns'); + } +}; diff --git a/resources/views/livewire/travel-request/create.blade.php b/resources/views/livewire/travel-request/create.blade.php index 95353f5..bdbf343 100644 --- a/resources/views/livewire/travel-request/create.blade.php +++ b/resources/views/livewire/travel-request/create.blade.php @@ -309,10 +309,42 @@ new #[Layout('components.layouts.app')] class extends Component { {{-- Journeys --}} -
+
- Journeys - + Journeys +
+ + +
@error('journeys')
{{ $message }}
@enderror @@ -328,13 +360,53 @@ new #[Layout('components.layouts.app')] class extends Component {
- - @error('journeys.'.$i.'.origin')
{{ $message }}
@enderror +
+ + +
+ @error('journeys.'.$i.'.origin')
{{ $message }}
@enderror
- - @error('journeys.'.$i.'.destination')
{{ $message }}
@enderror +
+ + +
+ @error('journeys.'.$i.'.destination')
{{ $message }}
@enderror
diff --git a/routes/console.php b/routes/console.php index 3c9adf1..451f66a 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,12 @@ comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Schedule::job(new SyncTowns)->dailyAt('02:00'); diff --git a/routes/web.php b/routes/web.php index 9042320..8c176fe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,7 @@ invalidate(); session()->regenerateToken(); + return redirect()->route('login'); })->name('logout')->middleware('auth'); +// Town autocomplete search +Route::middleware(['auth'])->get('/towns/search', function (Request $request) { + $query = $request->string('query')->trim(); + + if ($query->length() < 2) { + return response()->json([]); + } + + $towns = Town::active() + ->search($query) + ->when($request->filled('state'), fn ($q) => $q->where('state', $request->integer('state'))) + ->orderBy('town_name') + ->limit(10) + ->get(['town_name', 'state']); + + return response()->json( + $towns->map(fn ($town) => [ + 'name' => $town->town_name, + 'state' => $town->state->abbreviation(), + 'label' => $town->town_name.', '.$town->state->abbreviation(), + ]) + ); +})->name('towns.search'); + // Authenticated Route::middleware(['auth'])->group(function () { Route::livewire('/dashboard', 'dashboard')->name('dashboard');