This commit is contained in:
Bayu Lukman Yusuf 2026-01-20 12:24:31 +07:00
parent b0eda5e389
commit a2caec5b74
15 changed files with 1090 additions and 188 deletions

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Province;
use App\Models\District;
use App\Models\Subdistrict;
use App\Models\City;
use Illuminate\Http\Request;
class AddressController extends Controller
{
public function index(Request $request)
{
$addresses = auth()->user()->addresses;
return view('account.addresses', compact('addresses'));
}
public function provinces()
{
$provinces = Province::orderBy('name')->get();
return [
'data' => $provinces,
];
}
public function cities($provinceId)
{
$cities = City::where('province_id', $provinceId)->orderBy('name')->get();
return [
'data' => $cities,
];
}
public function districts($city_id)
{
$districts = District::where('city_id', $city_id)->orderBy('name')->get();
return [
'data' => $districts,
];
}
public function villages($districtId)
{
$villages = Subdistrict::where('district_id', $districtId)->orderBy('name')->get();
return [
'data' => $villages,
];
}
}

View File

@ -31,7 +31,7 @@ class SetLocale
// Check if locale is in session // Check if locale is in session
elseif (Session::has('locale')) { elseif (Session::has('locale')) {
$locale = Session::get('locale'); $locale = Session::get('locale') ?? 'id';
if (in_array($locale, ['en', 'id'])) { if (in_array($locale, ['en', 'id'])) {
App::setLocale($locale); App::setLocale($locale);

80
app/Models/Address.php Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class Address extends Model
{
use HasFactory;
use SoftDeletes;
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults();
}
protected $table = 'address';
protected $fillable = [
'label',
'name',
'address',
'province_id',
'city_id',
'district_id',
'subdistrict_id',
'province_name',
'city_name',
'district_name',
'subdistrict_name',
'postal_code',
'phone',
'longitude',
'latitude',
'user_id',
'is_primary'
];
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function province()
{
return $this->belongsTo(Province::class, 'province_id', 'id');
}
public function city()
{
return $this->belongsTo(City::class, 'city_id', 'id');
}
public function district()
{
return $this->belongsTo(District::class, 'district_id', 'id');
}
public function subdistrict()
{
return $this->belongsTo(Subdistrict::class, 'subdistrict_id', 'id');
}
public function getLocationAttribute()
{
$province = $this->province?->name;
$city = $this->city?->name;
$district = $this->district?->name;
$subdistrict = $this->subdistrict?->name;
$postalCode = $this->postal_code;
return "{$province}, {$city}, {$district}, {$subdistrict}, {$postalCode}";
}
}

18
app/Models/City.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class City extends Model
{
use HasFactory;
protected $fillable = ['province_id', 'name'];
public function province()
{
return $this->belongsTo(Province::class, 'province_id', 'id');
}
}

18
app/Models/District.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class District extends Model
{
use HasFactory;
protected $fillable = ['city_id', 'name'];
public function city()
{
return $this->belongsTo(City::class, 'city_id', 'id');
}
}

40
app/Models/PosCode.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PosCode extends Model
{
use HasFactory;
protected $fillable = [
'code',
'village',
'district',
'regency',
'province',
'latitude',
'longitude',
'elevation',
'timezone',
];
public function provinceRef(){
return $this->belongsTo(Province::class,"provice_id","id");
}
public function cityRef(){
return $this->belongsTo(City::class,"city_id","id");
}
public function districtRef(){
return $this->belongsTo(City::class,"district_id","id");
}
public function subdistrictRef(){
return $this->belongsTo(City::class,"subdistrict_id","id");
}
}

13
app/Models/Province.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Province extends Model
{
use HasFactory;
protected $fillable = ['name'];
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subdistrict extends Model
{
use HasFactory;
protected $fillable = ['district_id', 'name'];
public function district()
{
return $this->belongsTo(District::class, 'district_id', 'id');
}
}

View File

@ -56,4 +56,9 @@ class User extends Authenticatable
{ {
return $this->hasOne(Customer::class); return $this->hasOne(Customer::class);
} }
public function addresses()
{
return $this->hasMany(Address::class);
}
} }

View File

@ -0,0 +1,295 @@
<?php
namespace App\Repositories\Member\Address;
use App\Models\Address;
use App\Models\City;
use App\Models\District;
use App\Models\Province;
use App\Models\Subdistrict;
use App\Models\PosCode;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Cache;
class MemberAddressRepository
{
public function getList($request)
{
$model = Address::where('user_id', auth()->user()->id)->orderBy('created_at','desc')->paginate($request->limit);
return $model;
}
public function geocode($params){
$lat = @$params["latitude"];
$lon = @$params["longitude"];
$res = Cache::remember("geocode_$lat\_$lon", 60 * 60 * 24, function () use ($lat, $lon){
return Http::get("https://nominatim.openstreetmap.org/reverse",[
"email" => "husnusetiawan@gmail.com",
"lat" => $lat,
"lon" => $lon,
"format" => "json"
])->json();
});
try {
$address = @$res["address"];
$posCode = PosCode::where("code", $address['postcode'])->firstOrFail();
$province_id = $posCode->province_id ?? null;
$city_id = $posCode->city_id ?? null;
$district_id = $posCode->district_id ?? null;
$subdistrict_id = $posCode->subdistrict_id ?? null;
if ($address != null) {
$addr_province = $address["state"] ?? $address["city"] ?? $posCode->province;
$addr_city = $address["city"] ?? $address["county"] ?? $address["city_district"] ?? $posCode->city;
$addr_district = $address["suburb"] ?? $address["district"] ?? $posCode->district;
$addr_subdistrict = $address["neighbourhood"] ?? $address["village"] ?? $posCode->subdistrict;
if ($addr_province != null && $province_id == null) {
// search by nominatim
$province = Province::whereRaw('name ILIKE ?', ['%' . trim($addr_province) . '%'])->first();
// search by poscode
if ($province == null) {
$province = Province::whereRaw('name ILIKE ?', ['%' . trim($posCode->province) . '%'])->first();
}
$province_id = $posCode->province_id ?? $province->id ?? null;
}
if ($addr_city != null && $city_id == null) {
// search by nominatim
$city = City::whereRaw('name ILIKE ?', ['%' . trim($addr_city) . '%'])
->where("province_id", $province_id)
->first();
// search by poscode
if ($city == null) {
$city = City::whereRaw('name ILIKE ?', ['%' . trim($posCode->city) . '%'])
->where("province_id", $province_id)
->first();
}
$city_id = $posCode->city_id ?? $city->id ?? null;
}
if ($addr_district != null && $district_id == null) {
// search by nominatim
$district = District::whereRaw('name ILIKE ?', ['%' . trim($addr_district) . '%'])
->where("city_id", $city_id)
->first();
// search by poscode
if ($district == null) {
$district = District::whereRaw('name ILIKE ?', ['%' . trim($posCode->district) . '%'])
->where("city_id", $city_id)
->first();
}
$district_id = $posCode->district_id ?? $district->id ?? null;
}
if ($addr_subdistrict != null && $subdistrict_id == null) {
// search by nominatim
$subdistrict = Subdistrict::whereRaw('name ILIKE ?', ['%' . trim($addr_subdistrict) . '%'])
->where("district_id", $district_id)
->first();
// search by poscode
if ($subdistrict == null) {
$subdistrict = Subdistrict::whereRaw('name ILIKE ?', ['%' . trim($posCode->subdistrict) . '%'])
->where("district_id", $district_id)
->first();
}
$subdistrict_id = $posCode->subdistrict_id ?? $subdistrict->id ?? null;
}
}
$address = new Address([
'label' => "",
'name' => "",
'address' => $address["road"] ?? $res["display_name"],
'province_id' => $province_id,
'city_id' => $city_id,
'district_id' => $district_id,
'subdistrict_id' => $subdistrict_id,
'postal_code' => $address['postcode'],
'phone' => "",
'latitude' => $res['lat'],
'longitude' => $res['lon'],
'user_id' => auth()->user()->id,
'is_primary' => false
]);
} catch (ModelNotFoundException $e) {
abort(404, 'lokasi tidak didukung');
}
return $address;
}
public function create($data)
{
$model = DB::transaction(function () use ($data) {
$model = Address::create([
'label' => $data['label'],
'name' => $data['name'],
'address' => $data['address'],
'province_id' => $data['province_id'],
'city_id' => $data['city_id'],
'district_id' => $data['district_id'],
'subdistrict_id' => $data['subdistrict_id'],
'postal_code' => $data['postal_code'],
'phone' => $data['phone'],
'latitude' => $data['latitude'],
'longitude' => $data['longitude'],
'user_id' => auth()->user()->id,
'is_primary' => $data['is_primary']
]);
return $model;
});
return $model;
}
public function update($id ,$data)
{
$model = DB::transaction(function () use ($id, $data) {
$model = Address::findOrFail($id);
$model->label = $data['label'] ?? $model->label;
$model->name = $data['name'] ?? $model->name;
$model->address = $data['address'] ?? $model->address;
$model->province_id = $data['province_id'] ?? $model->province_id;
$model->city_id = $data['city_id'] ?? $model->city_id;
$model->district_id = $data['district_id'] ?? $model->district_id;
$model->subdistrict_id = $data['subdistrict_id'] ?? $model->subdistrict_id;
$model->postal_code = $data['postal_code'] ?? $model->postal_code;
$model->phone = $data['phone'] ?? $model->phone;
$model->latitude = $data['latitude'] ?? $model->latitude;
$model->longitude = $data['longitude'] ?? $model->longitude;
$model->is_primary = $data['is_primary'] ?? $model->is_primary;
$model->save();
return $model;
});
return $model;
}
public function setPrimary($id)
{
$model = DB::transaction(function () use ($id) {
$model = Address::findOrFail($id);
Address::where("user_id", $model->user_id)->update([
"is_primary" => 0
]);
$model->is_primary = 1;
$model->save();
return $model;
});
return $model;
}
public function delete($model)
{
$model = DB::transaction(function () use ($model) {
$model->delete();
return $model;
});
return $model;
}
public function getProvince($request)
{
$model = Province::orderBy('name','desc')->where("is_new",true)->paginate($request->limit);
return $model;
}
public function getCity($request)
{
$all = $request->all();
$model = City::orderBy('name','desc')->where("is_new",true)
->where(function($query) use ($all){
if (@$all["province_id"])
$query->where("province_id", $all["province_id"]);
})->paginate($request->limit);
return $model;
}
public function getDistrict($request)
{
$all = $request->all();
$model = District::orderBy('name','desc')->where("is_new",true)
->where(function($query) use ($all){
if (@$all["city_id"])
$query->where("city_id", $all["city_id"]);
})->paginate($request->limit);
return $model;
}
public function getSubdistrict($request)
{
$all = $request->all();
$model = Subdistrict::orderBy('name','desc')->where("is_new",true)
->where(function($query) use ($all){
if (@$all["district_id"])
$query->where("district_id", $all["district_id"]);
})
->paginate($request->limit);
return $model;
}
public function getPoscode($request)
{
$all = $request->all();
$model = PosCode::orderBy('province','desc')
->where(function($query) use ($all){
if (@$all["village"])
$query->where("village", $all["village"]);
if (@$all["district"])
$query->where("district", $all["district"]);
if (@$all["regency"])
$query->where("regency", $all["regency"]);
})
->paginate($request->limit);
return $model;
}
}

40
lang/en/addresses.php Normal file
View File

@ -0,0 +1,40 @@
<?php
return [
'page_title' => 'Addresses',
'shipping_address' => 'Shipping address',
'primary' => 'Primary',
'edit' => 'Edit',
'country' => 'Country',
'select_country' => 'Select country...',
'province' => 'Province',
'select_province' => 'Select province...',
'city' => 'City',
'select_city' => 'Select city...',
'district' => 'District',
'select_district' => 'Select district...',
'village' => 'Village',
'select_village' => 'Select village...',
'zip_code' => 'ZIP code',
'address' => 'Address',
'set_as_primary_address' => 'Set as primary address',
'save_changes' => 'Save changes',
'close' => 'Close',
'alternative_shipping_address' => 'Alternative shipping address',
'add_address' => 'Add address',
'please_select_country' => 'Please select your country!',
'please_select_province' => 'Please select your province!',
'please_select_city' => 'Please select your city!',
'please_select_district' => 'Please select your district!',
'please_select_village' => 'Please select your village!',
'please_enter_zip_code' => 'Please enter your ZIP code!',
'please_enter_address' => 'Please enter your address!',
'regions' => [
'africa' => 'Africa',
'asia' => 'Asia',
'europe' => 'Europe',
'north_america' => 'North America',
'south_america' => 'South America',
'oceania' => 'Oceania'
]
];

40
lang/id/addresses.php Normal file
View File

@ -0,0 +1,40 @@
<?php
return [
'page_title' => 'Alamat',
'shipping_address' => 'Alamat Pengiriman',
'primary' => 'Utama',
'edit' => 'Edit',
'country' => 'Negara',
'select_country' => 'Pilih negara...',
'province' => 'Provinsi',
'select_province' => 'Pilih provinsi...',
'city' => 'Kota',
'select_city' => 'Pilih kota...',
'district' => 'Kecamatan',
'select_district' => 'Pilih kecamatan...',
'village' => 'Kelurahan',
'select_village' => 'Pilih kelurahan...',
'zip_code' => 'Kode Pos',
'address' => 'Alamat',
'set_as_primary_address' => 'Jadikan alamat utama',
'save_changes' => 'Simpan perubahan',
'close' => 'Tutup',
'alternative_shipping_address' => 'Alamat Pengiriman Alternatif',
'add_address' => 'Tambah alamat',
'please_select_country' => 'Silakan pilih negara Anda!',
'please_select_province' => 'Silakan pilih provinsi Anda!',
'please_select_city' => 'Silakan pilih kota Anda!',
'please_select_district' => 'Silakan pilih kecamatan Anda!',
'please_select_village' => 'Silakan pilih kelurahan Anda!',
'please_enter_zip_code' => 'Silakan masukkan kode pos Anda!',
'please_enter_address' => 'Silakan masukkan alamat Anda!',
'regions' => [
'africa' => 'Afrika',
'asia' => 'Asia',
'europe' => 'Eropa',
'north_america' => 'Amerika Utara',
'south_america' => 'Amerika Selatan',
'oceania' => 'Oseania'
]
];

View File

@ -1,4 +1,4 @@
@extends('layouts.account', ['title' => 'Account - Addresses']) @extends('layouts.account', ['title' => __('addresses.page_title')])
@section('content') @section('content')
<!-- Addresses content --> <!-- Addresses content -->
@ -6,198 +6,177 @@
<div class="ps-lg-3 ps-xl-0"> <div class="ps-lg-3 ps-xl-0">
<!-- Page title --> <!-- Page title -->
<h1 class="h2 mb-1 mb-sm-2">Addresses</h1> <h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1>
<!-- Primary shipping address --> @foreach ($addresses as $address)
<div class="border-bottom py-4"> <div class="border-bottom py-4">
<div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3"> <div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3">
<div class="d-flex align-items-center gap-3 me-4"> <div class="d-flex align-items-center gap-3 me-4">
<h2 class="h6 mb-0">Shipping address</h2> <h2 class="h6 mb-0">{{ $address->label }}</h2>
<span class="badge text-bg-info rounded-pill">Primary</span> @if ($address->is_primary)
<span class="badge text-bg-info rounded-pill">{{ __('addresses.primary') }}</span>
@endif
</div>
<a class="nav-link hiding-collapse-toggle text-decoration-underline p-0 collapsed primaryAddressEditButton"
data-address-id="{{ $address->id }}" href=".primary-address-{{ $address->id }}"
data-bs-toggle="collapse" aria-expanded="false"
aria-controls="primaryAddressPreview{{ $address->id }} primaryAddressEdit{{ $address->id }}">{{ __('addresses.edit') }}</a>
</div>
<div class="collapse primary-address-{{ $address->id }} show"
id="primaryAddressPreview{{ $address->id }}">
<ul class="list-unstyled fs-sm m-0">
<li>{{ $address->location }}</li>
<li>{{ $address->address }}</li>
</ul>
</div>
<div class="collapse primary-address-{{ $address->id }}" id="primaryAddressEdit{{ $address->id }}">
<form class="row g-3 g-sm-4 needs-validation" novalidate>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label>
<select class="form-select province-select" data-select='{"searchEnabled": true}'
aria-label="Select city" required data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_province') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_province') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.city') }}</label>
<select class="form-select city-select" data-select='{"searchEnabled": true}'
aria-label="Select city" required data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_city') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_city') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.district') }}</label>
<select class="form-select district-select" data-select='{"searchEnabled": true}'
aria-label="Select district" required data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_district') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_district') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.village') }}</label>
<select class="form-select village-select" data-select='{"searchEnabled": true}'
aria-label="Select village" required data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_village') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div>
</div>
</div>
<div class="col-sm-4">
<div class="position-relative">
<label for="psa-zip-{{ $address->id }}"
class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control" id="psa-zip-{{ $address->id }}"
value="{{ $address->postal_code }}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div>
</div>
<div class="col-sm-8">
<div class="position-relative">
<label for="psa-address-{{ $address->id }}"
class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control" id="psa-address-{{ $address->id }}"
value="{{ $address->address }}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div>
</div>
<div class="col-12">
<div class="form-check mb-0">
<input type="checkbox" class="form-check-input" id="set-primary-{{ $address->id }}"
{{ $address->is_primary ? 'checked' : '' }}>
<label for="set-primary-{{ $address->id }}"
class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-3 pt-2 pt-sm-0">
<button type="submit"
class="btn btn-primary">{{ __('addresses.save_changes') }}</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse"
data-bs-target=".primary-address-{{ $address->id }}" aria-expanded="true"
aria-controls="primaryAddressPreview{{ $address->id }} primaryAddressEdit{{ $address->id }}">{{ __('addresses.close') }}</button>
</div>
</div>
</form>
</div> </div>
<a class="nav-link hiding-collapse-toggle text-decoration-underline p-0 collapsed" href=".primary-address"
data-bs-toggle="collapse" aria-expanded="false"
aria-controls="primaryAddressPreview primaryAddressEdit">Edit</a>
</div>
<div class="collapse primary-address show" id="primaryAddressPreview">
<ul class="list-unstyled fs-sm m-0">
<li>New York 11741, USA</li>
<li>396 Lillian Bolavandy, Holbrook</li>
</ul>
</div>
<div class="collapse primary-address" id="primaryAddressEdit">
<form class="row g-3 g-sm-4 needs-validation" novalidate>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">Country</label>
<select class="form-select" data-select='{"searchEnabled": true}'
aria-label="Select country" required>
<option value="">Select country...</option>
<optgroup label="Africa">
<option value="Nigeria">Nigeria</option>
<option value="South Africa">South Africa</option>
<option value="Kenya">Kenya</option>
<option value="Egypt">Egypt</option>
<option value="Ethiopia">Ethiopia</option>
</optgroup>
<optgroup label="Asia">
<option value="China">China</option>
<option value="India">India</option>
<option value="Japan">Japan</option>
<option value="South Korea">South Korea</option>
<option value="Saudi Arabia">Saudi Arabia</option>
</optgroup>
<optgroup label="Europe">
<option value="Germany">Germany</option>
<option value="France">France</option>
<option value="United Kingdom">United Kingdom</option>
<option value="Italy">Italy</option>
<option value="Spain">Spain</option>
</optgroup>
<optgroup label="North America">
<option value="United States" selected>United States</option>
<option value="Canada">Canada</option>
<option value="Mexico">Mexico</option>
<option value="Jamaica">Jamaica</option>
<option value="Costa Rica">Costa Rica</option>
</optgroup>
<optgroup label="South America">
<option value="Brazil">Brazil</option>
<option value="Argentina">Argentina</option>
<option value="Colombia">Colombia</option>
<option value="Chile">Chile</option>
<option value="Peru">Peru</option>
</optgroup>
<optgroup label="Oceania">
<option value="Australia">Australia</option>
<option value="New Zealand">New Zealand</option>
<option value="Papua New Guinea">Papua New Guinea</option>
<option value="Fiji">Fiji</option>
<option value="Solomon Islands">Solomon Islands</option>
</optgroup>
</select>
<div class="invalid-feedback">Please select your country!</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">City</label>
<select class="form-select" data-select='{"searchEnabled": true}' aria-label="Select city"
required>
<option value="">Select city...</option>
<option value="Austin">Austin</option>
<option value="Charlotte">Charlotte</option>
<option value="Chicago">Chicago</option>
<option value="Columbus">Columbus</option>
<option value="Dallas">Dallas</option>
<option value="Houston">Houston</option>
<option value="Jacksonville">Jacksonville</option>
<option value="Los Angeles">Los Angeles</option>
<option value="New York" selected>New York</option>
<option value="Orlando">Orlando</option>
<option value="Philadelphia">Philadelphia</option>
<option value="Phoenix">Phoenix</option>
<option value="San Antonio">San Antonio</option>
<option value="San Diego">San Diego</option>
<option value="San Jose">San Jose</option>
</select>
<div class="invalid-feedback">Please select your city!</div>
</div>
</div>
<div class="col-sm-4">
<div class="position-relative">
<label for="psa-zip" class="form-label">ZIP code</label>
<input type="text" class="form-control" id="psa-zip" value="11741" required>
<div class="invalid-feedback">Please enter your ZIP code!</div>
</div>
</div>
<div class="col-sm-8">
<div class="position-relative">
<label for="psa-address" class="form-label">Address</label>
<input type="text" class="form-control" id="psa-address"
value="396 Lillian Bolavandy, Holbrook" required>
<div class="invalid-feedback">Please enter your address!</div>
</div>
</div>
<div class="col-12">
<div class="form-check mb-0">
<input type="checkbox" class="form-check-input" id="set-primary-1" checked>
<label for="set-primary-1" class="form-check-label">Set as primary
address</label>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-3 pt-2 pt-sm-0">
<button type="submit" class="btn btn-primary">Save changes</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse"
data-bs-target=".primary-address" aria-expanded="true"
aria-controls="primaryAddressPreview primaryAddressEdit">Close</button>
</div>
</div>
</form>
</div> </div>
@endforeach
<!-- Add address button -->
<div class="nav pt-4">
<a class="nav-link animate-underline fs-base px-0" href="#newAddressModal" data-bs-toggle="modal">
<i class="ci-plus fs-lg ms-n1 me-2"></i>
<span class="animate-target">{{ __('addresses.add_address') }}</span>
</a>
</div> </div>
</div>
</div>
<!-- Alternative shipping address --> <!-- New Address Modal -->
<div class="border-bottom py-4"> <div class="modal fade" id="newAddressModal" tabindex="-1" aria-labelledby="newAddressModalLabel" aria-hidden="true">
<div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3"> <div class="modal-dialog modal-lg">
<div class="d-flex align-items-center gap-3 me-4"> <div class="modal-content">
<h2 class="h6 mb-0">Alternative shipping address</h2> <div class="modal-header">
</div> <h5 class="modal-title" id="newAddressModalLabel">{{ __('addresses.add_address') }}</h5>
<a class="nav-link hiding-collapse-toggle text-decoration-underline p-0 collapsed" <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
href=".alternative-address" data-bs-toggle="collapse" aria-expanded="false"
aria-controls="alternativeAddressPreview alternativeAddressEdit">Edit</a>
</div> </div>
<div class="collapse alternative-address show" id="alternativeAddressPreview"> <div class="modal-body">
<ul class="list-unstyled fs-sm m-0">
<li>Florida 32806, USA</li>
<li>514 S. Magnolia St., Orlando</li>
</ul>
</div>
<div class="collapse alternative-address" id="alternativeAddressEdit">
<form class="row g-3 g-sm-4 needs-validation" novalidate> <form class="row g-3 g-sm-4 needs-validation" novalidate>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">Country</label> <label class="form-label">{{ __('addresses.country') }}</label>
<select class="form-select" data-select='{"searchEnabled": true}' <select class="form-select" data-select='{"searchEnabled": true}'
aria-label="Select country" required> aria-label="Select country" required>
<option value="">Select country...</option> <option value="">{{ __('addresses.select_country') }}</option>
<optgroup label="Africa"> <optgroup label="{{ __('addresses.regions.africa') }}">
<option value="Nigeria">Nigeria</option> <option value="Nigeria">Nigeria</option>
<option value="South Africa">South Africa</option> <option value="South Africa">South Africa</option>
<option value="Kenya">Kenya</option> <option value="Kenya">Kenya</option>
<option value="Egypt">Egypt</option> <option value="Egypt">Egypt</option>
<option value="Ethiopia">Ethiopia</option> <option value="Ethiopia">Ethiopia</option>
</optgroup> </optgroup>
<optgroup label="Asia"> <optgroup label="{{ __('addresses.regions.asia') }}">
<option value="China">China</option> <option value="China">China</option>
<option value="India">India</option> <option value="India">India</option>
<option value="Japan">Japan</option> <option value="Japan">Japan</option>
<option value="South Korea">South Korea</option> <option value="South Korea">South Korea</option>
<option value="Saudi Arabia">Saudi Arabia</option> <option value="Saudi Arabia">Saudi Arabia</option>
</optgroup> </optgroup>
<optgroup label="Europe"> <optgroup label="{{ __('addresses.regions.europe') }}">
<option value="Germany">Germany</option> <option value="Germany">Germany</option>
<option value="France">France</option> <option value="France">France</option>
<option value="United Kingdom">United Kingdom</option> <option value="United Kingdom">United Kingdom</option>
<option value="Italy">Italy</option> <option value="Italy">Italy</option>
<option value="Spain">Spain</option> <option value="Spain">Spain</option>
</optgroup> </optgroup>
<optgroup label="North America"> <optgroup label="{{ __('addresses.regions.north_america') }}">
<option value="United States" selected>United States</option> <option value="United States">United States</option>
<option value="Canada">Canada</option> <option value="Canada">Canada</option>
<option value="Mexico">Mexico</option> <option value="Mexico">Mexico</option>
<option value="Jamaica">Jamaica</option> <option value="Jamaica">Jamaica</option>
<option value="Costa Rica">Costa Rica</option> <option value="Costa Rica">Costa Rica</option>
</optgroup> </optgroup>
<optgroup label="South America"> <optgroup label="{{ __('addresses.regions.south_america') }}">
<option value="Brazil">Brazil</option> <option value="Brazil">Brazil</option>
<option value="Argentina">Argentina</option> <option value="Argentina">Argentina</option>
<option value="Colombia">Colombia</option> <option value="Colombia">Colombia</option>
<option value="Chile">Chile</option> <option value="Chile">Chile</option>
<option value="Peru">Peru</option> <option value="Peru">Peru</option>
</optgroup> </optgroup>
<optgroup label="Oceania"> <optgroup label="{{ __('addresses.regions.oceania') }}">
<option value="Australia">Australia</option> <option value="Australia">Australia</option>
<option value="New Zealand">New Zealand</option> <option value="New Zealand">New Zealand</option>
<option value="Papua New Guinea">Papua New Guinea</option> <option value="Papua New Guinea">Papua New Guinea</option>
@ -205,15 +184,15 @@
<option value="Solomon Islands">Solomon Islands</option> <option value="Solomon Islands">Solomon Islands</option>
</optgroup> </optgroup>
</select> </select>
<div class="invalid-feedback">Please select your country!</div> <div class="invalid-feedback">{{ __('addresses.please_select_country') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="position-relative"> <div class="position-relative">
<label class="form-label">City</label> <label class="form-label">{{ __('addresses.city') }}</label>
<select class="form-select" data-select='{"searchEnabled": true}' <select class="form-select" data-select='{"searchEnabled": true}'
aria-label="Select city" required> aria-label="Select city" required>
<option value="">Select city...</option> <option value="">{{ __('addresses.select_city') }}</option>
<option value="Austin">Austin</option> <option value="Austin">Austin</option>
<option value="Charlotte">Charlotte</option> <option value="Charlotte">Charlotte</option>
<option value="Chicago">Chicago</option> <option value="Chicago">Chicago</option>
@ -223,61 +202,353 @@
<option value="Jacksonville">Jacksonville</option> <option value="Jacksonville">Jacksonville</option>
<option value="Los Angeles">Los Angeles</option> <option value="Los Angeles">Los Angeles</option>
<option value="New York">New York</option> <option value="New York">New York</option>
<option value="Orlando" selected>Orlando</option> <option value="Orlando">Orlando</option>
<option value="Philadelphia">Philadelphia</option> <option value="Philadelphia">Philadelphia</option>
<option value="Phoenix">Phoenix</option> <option value="Phoenix">Phoenix</option>
<option value="San Antonio">San Antonio</option> <option value="San Antonio">San Antonio</option>
<option value="San Diego">San Diego</option> <option value="San Diego">San Diego</option>
<option value="San Jose">San Jose</option> <option value="San Jose">San Jose</option>
</select> </select>
<div class="invalid-feedback">Please select your city!</div> <div class="invalid-feedback">{{ __('addresses.please_select_city') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="position-relative"> <div class="position-relative">
<label for="asa-zip" class="form-label">ZIP code</label> <label for="new-zip" class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control" id="asa-zip" value="32806" required> <input type="text" class="form-control" id="new-zip" required>
<div class="invalid-feedback">Please enter your ZIP code!</div> <div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div> </div>
</div> </div>
<div class="col-sm-8"> <div class="col-sm-8">
<div class="position-relative"> <div class="position-relative">
<label for="asa-address" class="form-label">Address</label> <label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control" id="asa-address" value="514 S. Magnolia St." <input type="text" class="form-control" id="new-address" required>
required> <div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
<div class="invalid-feedback">Please enter your address!</div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="form-check mb-0"> <div class="form-check mb-0">
<input type="checkbox" class="form-check-input" id="set-primary-2"> <input type="checkbox" class="form-check-input" id="set-primary-new">
<label for="set-primary-2" class="form-check-label">Set as primary <label for="set-primary-new"
address</label> class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-3 pt-2 pt-sm-0">
<button type="submit" class="btn btn-primary">Save changes</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse"
data-bs-target=".alternative-address" aria-expanded="true"
aria-controls="alternativeAddressPreview alternativeAddressEdit">Close</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> <div class="modal-footer">
<button type="button" class="btn btn-secondary"
<!-- Add address button --> data-bs-dismiss="modal">{{ __('addresses.close') }}</button>
<div class="nav pt-4"> <button type="submit" class="btn btn-primary">{{ __('addresses.save_changes') }}</button>
<a class="nav-link animate-underline fs-base px-0" href="#newAddressModal" data-bs-toggle="modal"> </div>
<i class="ci-plus fs-lg ms-n1 me-2"></i>
<span class="animate-target">Add address</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection
@section('scripts') @section('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add click event listener to edit buttons
document.addEventListener('click', function(e) {
// Check if clicked element is an edit button
if (e.target.closest('.primaryAddressEditButton')) {
// Find the button element to get its data attribute
const button = e.target.closest('.primaryAddressEditButton');
console.log('Button clicked:', button);
// Get address ID from data attribute
const addressId = button.getAttribute('data-address-id');
console.log('Address ID:', addressId);
if (addressId) {
loadProvincesForAddress(addressId);
}
}
});
function loadProvincesForAddress(addressId) {
console.log('Loading provinces for address ID:', addressId);
const provinceSelect = document.querySelector(`.province-select[data-address-id="${addressId}"]`);
if (!provinceSelect) {
console.log('Province select not found for address ID:', addressId);
return;
}
const currentProvinceId = '{{ $address->province_id }}';
const currentCityId = '{{ $address->city_id }}';
const currentDistrictId = '{{ $address->district_id }}';
const currentVillageId = '{{ $address->subdistrict_id }}';
// Load provinces via AJAX
fetch('/addresses/provinces')
.then(response => response.json())
.then(data => {
if (window.Choices) {
// Destroy existing instance if any
if (provinceSelect.choices) {
provinceSelect.choices.destroy();
}
}
// Clear and set options
provinceSelect.innerHTML =
'<option value="">{{ __('addresses.select_province') }}</option>';
data.data.forEach(province => {
const option = document.createElement('option');
option.value = province.id;
option.textContent = province.name;
// Select current province if it matches
if (province.id == currentProvinceId) {
option.selected = true;
}
provinceSelect.appendChild(option);
});
console.log(provinceSelect);
// Initialize Choices.js
if (window.Choices) {
// Destroy existing instance if any
if (provinceSelect.choices) {
provinceSelect.choices.destroy();
}
// Initialize new Choices instance
provinceSelect.choices = new window.Choices(provinceSelect, {
searchEnabled: true,
itemSelectText: 'Select an option',
noResultsText: 'No results found',
shouldSort: false
});
}
// Load cities if province is selected
if (currentProvinceId) {
loadCitiesForAddress(addressId, currentProvinceId, currentCityId, currentDistrictId,
currentVillageId);
}
})
.catch(error => console.log('Error loading provinces:', error));
}
// Add change event listener to province selects
document.addEventListener('change', function(e) {
if (e.target.classList.contains('province-select')) {
const addressId = e.target.dataset.addressId;
const provinceId = e.target.value;
// Clear district and village selects
const citySelect = document.querySelector(
`.city-select[data-address-id="${addressId}"]`);
const districtSelect = document.querySelector(
`.district-select[data-address-id="${addressId}"]`);
const villageSelect = document.querySelector(
`.village-select[data-address-id="${addressId}"]`);
if (citySelect) {
citySelect.innerHTML =
'<option value="">{{ __('addresses.select_city') }}</option>';
}
if (districtSelect) {
districtSelect.innerHTML =
'<option value="">{{ __('addresses.select_district') }}</option>';
}
if (villageSelect) {
villageSelect.innerHTML =
'<option value="">{{ __('addresses.select_village') }}</option>';
}
// Load cities if province is selected
if (provinceId) {
loadCitiesForAddress(addressId, provinceId);
}
}
if (e.target.classList.contains('city-select')) {
const addressId = e.target.dataset.addressId;
const cityId = e.target.value;
// Clear district and village selects
const districtSelect = document.querySelector(
`.district-select[data-address-id="${addressId}"]`);
const villageSelect = document.querySelector(
`.village-select[data-address-id="${addressId}"]`);
if (districtSelect) {
districtSelect.innerHTML =
'<option value="">{{ __('addresses.select_district') }}</option>';
}
if (villageSelect) {
villageSelect.innerHTML =
'<option value="">{{ __('addresses.select_village') }}</option>';
}
// Load districts if city is selected
if (cityId) {
loadDistrictsForAddress(addressId, cityId);
}
}
});
// Add change event listener to district selects
document.addEventListener('change', function(e) {
if (e.target.classList.contains('district-select')) {
const addressId = e.target.dataset.addressId;
const districtId = e.target.value;
// Clear village select
const villageSelect = document.querySelector(
`.village-select[data-address-id="${addressId}"]`);
if (villageSelect) {
villageSelect.innerHTML =
'<option value="">{{ __('addresses.select_village') }}</option>';
}
// Load villages if district is selected
if (districtId) {
loadVillages(addressId, districtId);
}
}
});
function loadCitiesForAddress(addressId, provinceId, currentCityId = null, currentDistrictId = null,
currentVillageId = null) {
fetch(`/addresses/cities/${provinceId}`)
.then(response => response.json())
.then(data => {
const citySelect = document.querySelector(
`.city-select[data-address-id="${addressId}"]`);
if (citySelect) {
// Clear and set options
citySelect.innerHTML =
'<option value="">{{ __('addresses.select_city') }}</option>';
data.data.forEach(city => {
const option = document.createElement('option');
option.value = city.id;
option.textContent = city.name;
if (currentCityId && city.id == currentCityId) {
option.selected = true;
}
citySelect.appendChild(option);
});
// Initialize Choices.js
if (window.Choices) {
// Destroy existing instance if any
if (citySelect.choices) {
citySelect.choices.destroy();
}
// Initialize new Choices instance
citySelect.choices = new window.Choices(citySelect, {
searchEnabled: true,
itemSelectText: 'Select an option',
noResultsText: 'No results found',
shouldSort: false
});
}
// Load districts if city is selected
if (currentCityId) {
loadDistrictsForAddress(addressId, currentCityId, currentDistrictId,
currentVillageId);
}
}
})
.catch(error => console.log('Error loading cities:', error));
}
function loadDistrictsForAddress(addressId, cityId, currentDistrictId = null, currentVillageId = null) {
fetch(`/addresses/districts/${cityId}`)
.then(response => response.json())
.then(data => {
const districtSelect = document.querySelector(
`.district-select[data-address-id="${addressId}"]`);
if (districtSelect) {
// Clear and set options
districtSelect.innerHTML =
'<option value="">{{ __('addresses.select_district') }}</option>';
data.data.forEach(district => {
const option = document.createElement('option');
option.value = district.id;
option.textContent = district.name;
if (currentDistrictId && district.id == currentDistrictId) {
option.selected = true;
}
districtSelect.appendChild(option);
});
// Initialize Choices.js
if (window.Choices) {
// Destroy existing instance if any
if (districtSelect.choices) {
districtSelect.choices.destroy();
}
// Initialize new Choices instance
districtSelect.choices = new window.Choices(districtSelect, {
searchEnabled: true,
itemSelectText: 'Select an option',
noResultsText: 'No results found',
shouldSort: false
});
}
// Load villages if district is selected
if (currentDistrictId) {
loadVillages(addressId, currentDistrictId, currentVillageId);
}
}
})
.catch(error => console.log('Error loading districts:', error));
}
function loadVillages(addressId, districtId, currentVillageId = null) {
fetch(`/addresses/villages/${districtId}`)
.then(response => response.json())
.then(data => {
const villageSelect = document.querySelector(
`.village-select[data-address-id="${addressId}"]`);
if (villageSelect) {
// Clear and set options
villageSelect.innerHTML =
'<option value="">{{ __('addresses.select_village') }}</option>';
data.data.forEach(village => {
const option = document.createElement('option');
option.value = village.id;
option.textContent = village.name;
if (currentVillageId && village.id == currentVillageId) {
option.selected = true;
}
villageSelect.appendChild(option);
});
// Initialize Choices.js
if (window.Choices) {
// Destroy existing instance if any
if (villageSelect.choices) {
villageSelect.choices.destroy();
}
// Initialize new Choices instance
villageSelect.choices = new window.Choices(villageSelect, {
searchEnabled: true,
itemSelectText: 'Select an option',
noResultsText: 'No results found',
shouldSort: false
});
}
}
})
.catch(error => console.log('Error loading villages:', error));
}
});
</script>
@endsection @endsection

View File

@ -68,12 +68,12 @@
<h6 class="pt-4 ps-2 ms-1">{{ __('account_sidebar.manage_account') }}</h6> <h6 class="pt-4 ps-2 ms-1">{{ __('account_sidebar.manage_account') }}</h6>
<nav class="list-group list-group-borderless"> <nav class="list-group list-group-borderless">
<a class="list-group-item list-group-item-action d-flex align-items-center" <a class="list-group-item list-group-item-action d-flex align-items-center"
href="{{ route('second', ['account', 'info']) }}"> href="{{ route('profile') }}">
<i class="ci-user fs-base opacity-75 me-2"></i> <i class="ci-user fs-base opacity-75 me-2"></i>
{{ __('account_sidebar.personal_info') }} {{ __('account_sidebar.personal_info') }}
</a> </a>
<a class="list-group-item list-group-item-action d-flex align-items-center" <a class="list-group-item list-group-item-action d-flex align-items-center"
href="{{ route('second', ['account', 'addresses']) }}"> href="{{ route('addresses') }}">
<i class="ci-map-pin fs-base opacity-75 me-2"></i> <i class="ci-map-pin fs-base opacity-75 me-2"></i>
{{ __('account_sidebar.addresses') }} {{ __('account_sidebar.addresses') }}
</a> </a>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\Auth\AddressController;
use App\Http\Controllers\Auth\LoginEmailController; use App\Http\Controllers\Auth\LoginEmailController;
use App\Http\Controllers\Auth\LoginWaController; use App\Http\Controllers\Auth\LoginWaController;
use App\Http\Controllers\Auth\RegisterController; use App\Http\Controllers\Auth\RegisterController;
@ -71,3 +72,10 @@ Route::group(['prefix' => '/login/google'], function () {
Route::get('/profile', [ProfileController::class, 'index'])->name('profile'); Route::get('/profile', [ProfileController::class, 'index'])->name('profile');
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password.update'); Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password.update');
Route::get('/addresses', [AddressController::class, 'index'])->name('addresses');
Route::get('/addresses/provinces', [AddressController::class, 'provinces'])->name('addresses.provinces');
Route::get('/addresses/cities/{provinceId}', [AddressController::class, 'cities'])->name('addresses.cities');
Route::get('/addresses/districts/{provinceId}', [AddressController::class, 'districts'])->name('addresses.districts');
Route::get('/addresses/villages/{districtId}', [AddressController::class, 'villages'])->name('addresses.villages');