load address using ajax
This commit is contained in:
parent
10f3abc047
commit
a86ba91db7
|
|
@ -13,7 +13,32 @@ class AddressController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$addresses = auth()->user()->addresses;
|
$addresses = auth()->user()->addresses()->orderBy('is_primary', 'desc')->get();
|
||||||
|
|
||||||
|
// If AJAX request, return JSON
|
||||||
|
if ($request->ajax() || $request->wantsJson()) {
|
||||||
|
$addressesData = $addresses->map(function ($address) {
|
||||||
|
return [
|
||||||
|
'id' => $address->id,
|
||||||
|
'label' => $address->label,
|
||||||
|
'location' => $address->location,
|
||||||
|
'address' => $address->address,
|
||||||
|
'is_primary' => $address->is_primary,
|
||||||
|
'province_id' => $address->province_id,
|
||||||
|
'city_id' => $address->city_id,
|
||||||
|
'district_id' => $address->district_id,
|
||||||
|
'subdistrict_id' => $address->subdistrict_id,
|
||||||
|
'postal_code' => $address->postal_code,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'addresses' => $addressesData
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular page load, return view
|
||||||
return view('account.addresses', compact('addresses'));
|
return view('account.addresses', compact('addresses'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ return [
|
||||||
'cannot_delete_only_address' => 'Cannot delete the only address',
|
'cannot_delete_only_address' => 'Cannot delete the only address',
|
||||||
'delete' => 'Delete',
|
'delete' => 'Delete',
|
||||||
'cancel' => 'Cancel',
|
'cancel' => 'Cancel',
|
||||||
|
'loading' => 'Loading...',
|
||||||
|
'notification' => 'Notification',
|
||||||
'confirm_delete_address' => 'Are you sure you want to delete this address?',
|
'confirm_delete_address' => 'Are you sure you want to delete this address?',
|
||||||
'this_action_cannot_be_undone' => 'This action cannot be undone.',
|
'this_action_cannot_be_undone' => 'This action cannot be undone.',
|
||||||
'regions' => [
|
'regions' => [
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,10 @@ return [
|
||||||
'cannot_delete_only_address' => 'Tidak dapat menghapus satu-satunya alamat',
|
'cannot_delete_only_address' => 'Tidak dapat menghapus satu-satunya alamat',
|
||||||
'delete' => 'Hapus',
|
'delete' => 'Hapus',
|
||||||
'cancel' => 'Batal',
|
'cancel' => 'Batal',
|
||||||
|
'loading' => 'Memuat...',
|
||||||
'confirm_delete_address' => 'Apakah Anda yakin ingin menghapus alamat ini?',
|
'confirm_delete_address' => 'Apakah Anda yakin ingin menghapus alamat ini?',
|
||||||
'this_action_cannot_be_undone' => 'Tindakan ini tidak dapat dibatalkan.',
|
'this_action_cannot_be_undone' => 'Tindakan ini tidak dapat dibatalkan.',
|
||||||
|
'notification' => 'Notifikasi',
|
||||||
'regions' => [
|
'regions' => [
|
||||||
'africa' => 'Afrika',
|
'africa' => 'Afrika',
|
||||||
'asia' => 'Asia',
|
'asia' => 'Asia',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,53 @@
|
||||||
@extends('layouts.account', ['title' => __('addresses.page_title')])
|
@extends('layouts.account', ['title' => __('addresses.page_title')])
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
<style>
|
||||||
|
.shimmer-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shimmer {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shimmer-line {
|
||||||
|
height: 0.75rem;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e9ecef 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shimmer-line.short {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#addresses-loading {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addresses-container {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<!-- Addresses content -->
|
<!-- Addresses content -->
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="ps-lg-3 ps-xl-0">
|
<div class="ps-lg-3 ps-xl-0">
|
||||||
|
|
@ -8,137 +55,31 @@
|
||||||
<!-- Page title -->
|
<!-- Page title -->
|
||||||
<h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1>
|
<h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1>
|
||||||
|
|
||||||
@foreach ($addresses as $address)
|
<!-- Loading indicator -->
|
||||||
<div class="border-bottom py-4">
|
<div id="addresses-loading" class="text-center py-4">
|
||||||
<div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3">
|
<div class="shimmer-wrapper">
|
||||||
<div class="d-flex align-items-center gap-3 me-4">
|
<div class="shimmer">
|
||||||
<h2 class="h6 mb-0">{{ $address->label }}</h2>
|
<div class="shimmer-line"></div>
|
||||||
@if ($address->is_primary)
|
<div class="shimmer-line"></div>
|
||||||
<span class="badge text-bg-info rounded-pill">{{ __('addresses.primary') }}</span>
|
<div class="shimmer-line short"></div>
|
||||||
@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>
|
||||||
<div class="collapse primary-address-{{ $address->id }} show"
|
<div class="shimmer">
|
||||||
id="primaryAddressPreview{{ $address->id }}">
|
<div class="shimmer-line"></div>
|
||||||
<ul class="list-unstyled fs-sm m-0">
|
<div class="shimmer-line"></div>
|
||||||
<li>{{ $address->location }}</li>
|
<div class="shimmer-line short"></div>
|
||||||
<li>{{ $address->address }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse primary-address-{{ $address->id }}" id="primaryAddressEdit{{ $address->id }}">
|
<div class="shimmer">
|
||||||
|
<div class="shimmer-line"></div>
|
||||||
{{-- add message on success or error --}}
|
<div class="shimmer-line"></div>
|
||||||
<div class="alert alert-danger d-none address-form-message" role="alert"></div>
|
<div class="shimmer-line short"></div>
|
||||||
<div class="alert alert-success d-none address-form-success" role="alert"></div>
|
|
||||||
|
|
||||||
<form class="row g-3 g-sm-4 needs-validation address-form" novalidate
|
|
||||||
action="{{ route('addresses.update', $address->id) }}" method="POST">
|
|
||||||
@csrf
|
|
||||||
@method('PUT')
|
|
||||||
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<div class="position-relative">
|
|
||||||
<label class="form-label">{{ __('addresses.province') }}</label>
|
|
||||||
<select class="form-select province-select" required
|
|
||||||
data-address-id="{{ $address->id }}"
|
|
||||||
data-province-id="{{ $address->province_id }}"
|
|
||||||
data-city-id="{{ $address->city_id }}"
|
|
||||||
data-district-id="{{ $address->district_id }}"
|
|
||||||
data-village-id="{{ $address->subdistrict_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" 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" 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" 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 d-flex justify-content-between">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-danger delete-address-btn"
|
|
||||||
data-address-id="{{ $address->id }}"
|
|
||||||
data-address-label="{{ $address->label }}">{{ __('addresses.delete') }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
<p class="text-muted mt-3">{{ __('addresses.loading') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Addresses container -->
|
||||||
|
<div id="addresses-container">
|
||||||
<!-- Add address button -->
|
<!-- Addresses will be loaded here via AJAX -->
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,6 +177,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification Modal -->
|
||||||
|
<div class="modal fade" id="notificationModal" tabindex="-1" aria-labelledby="notificationModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="notificationModalLabel">{{ __('addresses.notification') }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="notification-icon me-3">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
<div class="notification-message" id="notificationMessage"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div class="modal fade" id="deleteAddressModal" tabindex="-1" aria-labelledby="deleteAddressModalLabel"
|
<div class="modal fade" id="deleteAddressModal" tabindex="-1" aria-labelledby="deleteAddressModalLabel"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
|
|
@ -270,6 +231,218 @@
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
* LOAD ADDRESSES VIA AJAX
|
||||||
|
* =============================== */
|
||||||
|
function loadAddresses() {
|
||||||
|
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||||
|
const loadingElement = document.getElementById('addresses-loading');
|
||||||
|
const containerElement = document.getElementById('addresses-container');
|
||||||
|
|
||||||
|
// Show loading with shimmer
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.style.opacity = '1';
|
||||||
|
loadingElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
if (containerElement) {
|
||||||
|
containerElement.style.opacity = '0';
|
||||||
|
containerElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/addresses', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': token,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.addresses) {
|
||||||
|
renderAddresses(data.addresses);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load addresses:', data);
|
||||||
|
showToast('Failed to load addresses', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading addresses:', error);
|
||||||
|
showToast('Error loading addresses', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Smooth transition from loading to content
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingElement.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
if (containerElement) {
|
||||||
|
containerElement.style.opacity = '1';
|
||||||
|
containerElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadAddresses() {
|
||||||
|
loadAddresses();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAddresses(addresses) {
|
||||||
|
const container = document.getElementById('addresses-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
addresses.forEach(address => {
|
||||||
|
html += `
|
||||||
|
<div class="border-bottom py-4">
|
||||||
|
<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">
|
||||||
|
<h2 class="h6 mb-0">${address.label}</h2>
|
||||||
|
${address.is_primary ? '<span class="badge text-bg-info rounded-pill">{{ __("addresses.primary") }}</span>' : ''}
|
||||||
|
</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}">
|
||||||
|
<div class="alert alert-danger d-none address-form-message" role="alert"></div>
|
||||||
|
<div class="alert alert-success d-none address-form-success" role="alert"></div>
|
||||||
|
<form class="row g-3 g-sm-4 needs-validation address-form" novalidate
|
||||||
|
action="/addresses/${address.id}" method="POST">
|
||||||
|
<input type="hidden" name="_method" value="PUT">
|
||||||
|
<input type="hidden" name="_token" value="${document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''}">
|
||||||
|
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="position-relative">
|
||||||
|
<label class="form-label">{{ __('addresses.province') }}</label>
|
||||||
|
<select class="form-select province-select" required
|
||||||
|
data-address-id="${address.id}"
|
||||||
|
data-province-id="${address.province_id}"
|
||||||
|
data-city-id="${address.city_id}"
|
||||||
|
data-district-id="${address.district_id}"
|
||||||
|
data-village-id="${address.subdistrict_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" 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" 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" 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 class="form-label">{{ __('addresses.zip_code') }}</label>
|
||||||
|
<input type="text" class="form-control postal_code" 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 class="form-label">{{ __('addresses.address') }}</label>
|
||||||
|
<input type="text" class="form-control zip_code" 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" ${address.is_primary ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-between">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="btn btn-danger delete-address-btn"
|
||||||
|
data-address-id="${address.id}"
|
||||||
|
data-address-label="${address.label}">{{ __('addresses.delete') }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add address button
|
||||||
|
html += `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Initialize address forms after rendering
|
||||||
|
initializeAddressForms();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAddressForms() {
|
||||||
|
// Initialize province selects for each address
|
||||||
|
document.querySelectorAll('.province-select').forEach(select => {
|
||||||
|
const addressId = select.dataset.addressId;
|
||||||
|
const provinceId = select.dataset.provinceId;
|
||||||
|
const cityId = select.dataset.cityId;
|
||||||
|
const districtId = select.dataset.districtId;
|
||||||
|
const villageId = select.dataset.villageId;
|
||||||
|
|
||||||
|
// Load provinces and set initial values
|
||||||
|
loadProvinces(select, addressId, provinceId, cityId, districtId, villageId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load addresses on page load
|
||||||
|
loadAddresses();
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
* INIT EDIT BUTTON
|
* INIT EDIT BUTTON
|
||||||
* =============================== */
|
* =============================== */
|
||||||
|
|
@ -721,19 +894,19 @@
|
||||||
console.log(result);
|
console.log(result);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Show success message and reload page
|
// Show success message and reload page
|
||||||
showToast(result.message, 'success');
|
showNotification(result.message, 'success');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
reloadAddresses();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// Show error message
|
// Show error message
|
||||||
showToast(result.message || '{{ __('addresses.error_saving_address') }}',
|
showNotification(result.message || '{{ __('addresses.error_saving_address') }}',
|
||||||
'error');
|
'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showToast('{{ __('addresses.error_saving_address') }}', 'error');
|
showNotification('{{ __('addresses.error_saving_address') }}', 'error');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
deleteAddressId = null;
|
deleteAddressId = null;
|
||||||
|
|
@ -764,30 +937,46 @@
|
||||||
.addressId;
|
.addressId;
|
||||||
|
|
||||||
// Prepare form data for AJAX submission
|
// Prepare form data for AJAX submission
|
||||||
const submitData = {
|
const submitData = {};
|
||||||
_token: formData.get('_token'),
|
|
||||||
province_id: isNewAddress ?
|
// Get CSRF token
|
||||||
form.querySelector('.new-province-select').value : form.querySelector(
|
const tokenInput = form.querySelector('input[name="_token"]');
|
||||||
'.province-select').value,
|
if (tokenInput) submitData._token = tokenInput.value;
|
||||||
city_id: isNewAddress ?
|
|
||||||
form.querySelector('.new-city-select').value : form.querySelector(
|
// Get province ID
|
||||||
'.city-select').value,
|
const provinceSelect = isNewAddress ?
|
||||||
district_id: isNewAddress ?
|
form.querySelector('.new-province-select') : form.querySelector('.province-select');
|
||||||
form.querySelector('.new-district-select').value : form.querySelector(
|
if (provinceSelect) submitData.province_id = provinceSelect.value;
|
||||||
'.district-select').value,
|
|
||||||
subdistrict_id: isNewAddress ?
|
// Get city ID
|
||||||
form.querySelector('.new-village-select').value : form.querySelector(
|
const citySelect = isNewAddress ?
|
||||||
'.village-select').value,
|
form.querySelector('.new-city-select') : form.querySelector('.city-select');
|
||||||
postal_code: isNewAddress ?
|
if (citySelect) submitData.city_id = citySelect.value;
|
||||||
form.querySelector('#new-zip').value : form.querySelector(
|
|
||||||
'input[id^="psa-zip-"]').value,
|
// Get district ID
|
||||||
address: isNewAddress ?
|
const districtSelect = isNewAddress ?
|
||||||
form.querySelector('#new-address').value : form.querySelector(
|
form.querySelector('.new-district-select') : form.querySelector('.district-select');
|
||||||
'input[id^="psa-address-"]').value,
|
if (districtSelect) submitData.district_id = districtSelect.value;
|
||||||
is_primary: (isNewAddress ?
|
|
||||||
form.querySelector('#set-primary-new') :
|
// Get subdistrict ID
|
||||||
form.querySelector('input[id^="set-primary-"]')).checked ? '1' : '0'
|
const villageSelect = isNewAddress ?
|
||||||
};
|
form.querySelector('.new-village-select') : form.querySelector('.village-select');
|
||||||
|
if (villageSelect) submitData.subdistrict_id = villageSelect.value;
|
||||||
|
|
||||||
|
// Get postal code
|
||||||
|
const zipInput = isNewAddress ?
|
||||||
|
form.querySelector('#new-zip') : form.querySelector('.postal_code');
|
||||||
|
if (zipInput) submitData.postal_code = zipInput.value;
|
||||||
|
|
||||||
|
// Get address
|
||||||
|
const addressInput = isNewAddress ?
|
||||||
|
form.querySelector('#new-address') : form.querySelector('.zip_code');
|
||||||
|
if (addressInput) submitData.address = addressInput.value;
|
||||||
|
|
||||||
|
// Get primary checkbox
|
||||||
|
const primaryCheckbox = isNewAddress ?
|
||||||
|
form.querySelector('#set-primary-new') : form.querySelector('input[id^="set-primary-"]');
|
||||||
|
if (primaryCheckbox) submitData.is_primary = primaryCheckbox.checked ? '1' : '0';
|
||||||
|
|
||||||
if (!isNewAddress) {
|
if (!isNewAddress) {
|
||||||
submitData._method = 'PUT';
|
submitData._method = 'PUT';
|
||||||
|
|
@ -824,7 +1013,7 @@
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const modal = bootstrap.Modal.getInstance(newAddressModal);
|
const modal = bootstrap.Modal.getInstance(newAddressModal);
|
||||||
if (modal) modal.hide();
|
if (modal) modal.hide();
|
||||||
window.location.reload();
|
reloadAddresses();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
// Close the edit form and show preview after delay
|
// Close the edit form and show preview after delay
|
||||||
|
|
@ -840,7 +1029,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload page to show updated data
|
// Reload page to show updated data
|
||||||
window.location.reload();
|
reloadAddresses();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -886,13 +1075,15 @@
|
||||||
|
|
||||||
// Toast notification helper
|
// Toast notification helper
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
// Create toast element if it doesn't exist
|
console.log('Showing toast:', message, type);
|
||||||
|
|
||||||
|
// Create toast container if it doesn't exist
|
||||||
let toastContainer = document.getElementById('toast-container');
|
let toastContainer = document.getElementById('toast-container');
|
||||||
if (!toastContainer) {
|
if (!toastContainer) {
|
||||||
toastContainer = document.createElement('div');
|
toastContainer = document.createElement('div');
|
||||||
toastContainer.id = 'toast-container';
|
toastContainer.id = 'toast-container';
|
||||||
toastContainer.className = 'position-fixed top-0 end-0 p-3';
|
toastContainer.className = 'position-fixed top-0 end-0 p-3';
|
||||||
toastContainer.style.zIndex = '1050';
|
toastContainer.style.zIndex = '1055';
|
||||||
document.body.appendChild(toastContainer);
|
document.body.appendChild(toastContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,25 +1094,52 @@
|
||||||
<div class="toast-body">
|
<div class="toast-body">
|
||||||
${message}
|
${message}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||||
|
|
||||||
|
// Show toast using Bootstrap if available, otherwise fallback to simple alert
|
||||||
const toastElement = document.getElementById(toastId);
|
const toastElement = document.getElementById(toastId);
|
||||||
const toast = new bootstrap.Toast(toastElement, {
|
if (typeof bootstrap !== 'undefined' && bootstrap.Toast) {
|
||||||
autohide: true,
|
try {
|
||||||
delay: 3000
|
const toast = new bootstrap.Toast(toastElement, {
|
||||||
});
|
autohide: true,
|
||||||
|
delay: 3000
|
||||||
|
});
|
||||||
|
toast.show();
|
||||||
|
|
||||||
|
// Remove toast element after hidden
|
||||||
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||||
|
toastElement.remove();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Bootstrap Toast error:', error);
|
||||||
|
// Fallback to simple notification
|
||||||
|
toastElement.style.display = 'block';
|
||||||
|
toastElement.style.position = 'fixed';
|
||||||
|
toastElement.style.top = '20px';
|
||||||
|
toastElement.style.right = '20px';
|
||||||
|
toastElement.style.zIndex = '1055';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toastElement.style.display = 'none';
|
||||||
|
toastElement.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback for when Bootstrap is not available
|
||||||
|
console.warn('Bootstrap Toast not available, using fallback');
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toast.show();
|
// Toast notification helper (deprecated - use showNotification instead)
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
// Remove toast element after hidden
|
console.warn('showToast is deprecated, use showNotification instead');
|
||||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
showNotification(message, type);
|
||||||
toastElement.remove();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue