ECOMMERCE/resources/views/account/addresses.blade.php

1321 lines
64 KiB
PHP

@extends('layouts.account', ['title' => __('addresses.page_title')])
@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 -->
<div class="col-lg-9">
<div class="ps-lg-3 ps-xl-0">
<!-- Page title -->
<h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1>
<!-- Loading indicator -->
<div id="addresses-loading" class="text-center py-4">
<div class="shimmer-wrapper">
<div class="shimmer">
<div class="shimmer-line"></div>
<div class="shimmer-line"></div>
<div class="shimmer-line short"></div>
</div>
<div class="shimmer">
<div class="shimmer-line"></div>
<div class="shimmer-line"></div>
<div class="shimmer-line short"></div>
</div>
<div class="shimmer">
<div class="shimmer-line"></div>
<div class="shimmer-line"></div>
<div class="shimmer-line short"></div>
</div>
</div>
<p class="text-muted mt-3">{{ __('addresses.loading') }}</p>
</div>
<!-- Addresses container -->
<div id="addresses-container">
<!-- Addresses will be loaded here via AJAX -->
</div>
</div>
</div>
<!-- New Address Modal -->
<div class="modal fade" id="newAddressModal" tabindex="-1" aria-labelledby="newAddressModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<form class="needs-validation new-address-form" novalidate action="{{ route('addresses.store') }}"
method="POST">
@csrf
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newAddressModalLabel">{{ __('addresses.add_address') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row g-3 g-sm-4 ">
<div class="alert alert-danger d-none new-address-form-message" role="alert"></div>
<div class="alert alert-success d-none new-address-form-success" role="alert"></div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.label') }}</label>
<input type="text" class="form-control new-label" id="new-label" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_label') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.name') }}</label>
<input type="text" class="form-control new-name" id="new-name" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_name') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.phone') }}</label>
<input type="number" class="form-control new-phone" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_phone') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label>
<select class="form-select new-province-select" required>
<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 new-city-select" required>
<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 new-district-select" required>
<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 new-village-select" required>
<option value="">{{ __('addresses.select_village') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="number" class="form-control new-zip" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div>
</div>
<div class="col-12">
<x-address.address-map
:latitude="null"
:longitude="null"
latitudeInputName="latitude"
longitudeInputName="longitude"
:searchPlaceholder="__('addresses.search_for_location')"
:searchButtonText="__('addresses.search')"
:mapLabel="__('addresses.select_location_on_map')"
:instructionText="__('addresses.drag_marker_to_adjust')"
:typeToSearchText="__('addresses.type_to_search')" />
</div>
<div class="col-12">
<div class="position-relative">
<label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
<textarea class="form-control" id="new-address" rows="3" required></textarea>
<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-new">
<label for="set-primary-new"
class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ __('addresses.save_changes') }}</button>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">{{ __('addresses.close') }}</button>
</div>
</div>
</form>
</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 -->
<div class="modal fade" id="deleteAddressModal" tabindex="-1" aria-labelledby="deleteAddressModalLabel"
aria-hidden="true">
<div class="modal fade" id="newAddressModal" tabindex="-1" aria-labelledby="newAddressModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteAddressModalLabel">{{ __('addresses.delete') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>{{ __('addresses.confirm_delete_address') }}</p>
<div class="alert alert-warning">
<strong id="deleteAddressLabel"></strong>
</div>
<p class="text-muted small mb-0">{{ __('addresses.this_action_cannot_be_undone') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">{{ __('addresses.cancel') }}</button>
<button type="button" class="btn btn-danger"
id="confirmDeleteBtn">{{ __('addresses.delete') }}</button>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
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.label') }}</label>
<input type="text" class="form-control edit-label" value="${address.label}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.name') }}</label>
<input type="text" class="form-control edit-name" value="${address.name ?? ''}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_name') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.phone') }}</label>
<input type="tel" class="form-control edit-phone" value="${address.phone ?? ''}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_phone') }}</div>
</div>
</div>
<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-6">
<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-12">
<div class="address-map-component">
<!-- Hidden inputs for latitude and longitude -->
<input type="hidden" name="latitude" id="latitude" value="${address.latitude ?? ''}" class="latitude address-latitude-input" step="0.000001">
<input type="hidden" name="longitude" id="longitude" value="${address.longitude ?? ''}" class="longitude address-longitude-input" step="0.000001">
<!-- Map interface -->
<div class="position-relative">
<label class="form-label">{{ __('addresses.select_location_on_map') }}</label>
<div class="mb-3 position-relative">
<div class="input-group">
<input type="text" class="form-control address-location-search" placeholder="{{ __('addresses.search_for_location') }}" autocomplete="off">
<button class="btn btn-outline-secondary address-search-btn" type="button">
<i class="ci-search"></i> {{ __('addresses.search') }}
</button>
</div>
<!-- Search suggestions dropdown -->
<div class="address-search-suggestions position-absolute w-100 bg-white border rounded-top-0 rounded-bottom shadow-sm" style="z-index: 1050; max-height: 200px; overflow-y: auto; display: none;">
<div class="p-2 text-muted small">{{ __('addresses.type_to_search') }}</div>
</div>
</div>
<div class="address-map-container" style="height: 400px; width: 100%; border-radius: 8px; overflow: hidden;"></div>
<small class="text-muted">{{ __('addresses.drag_marker_to_adjust') }}</small>
</div>
</div>
</div>
<div class="col-12">
<div class="position-relative">
<label class="form-label">{{ __('addresses.address') }}</label>
<textarea class="form-control address-input" rows="3" required>${address.address}</textarea>
<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 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);
});
// Initialize map components (for dynamically generated content)
document.querySelectorAll('.address-map-component').forEach(component => {
// Trigger initialization for this component
if (window.initializeAddressMap) {
const componentIndex = Date.now() + Math.random();
window.initializeAddressMap(component, componentIndex);
}
});
}
// Load addresses on page load
loadAddresses();
/* ===============================
* INIT EDIT BUTTON
* =============================== */
document.addEventListener('click', e => {
const btn = e.target.closest('.primaryAddressEditButton');
if (!btn) return;
const addressId = btn.dataset.addressId;
loadProvinces(addressId);
});
/* ===============================
* PROVINCE
* =============================== */
function loadProvinces(addressId) {
const province = document.querySelector(
`.province-select[data-address-id="${addressId}"]`
);
if (!province) return;
const {
provinceId,
cityId,
districtId,
villageId
} = province.dataset;
destroyChoices(province);
fetch('/addresses/provinces')
.then(r => r.json())
.then(({
data
}) => {
province.innerHTML =
`<option value="">{{ __('addresses.select_province') }}</option>`;
data.forEach(p => {
province.insertAdjacentHTML(
'beforeend',
`<option value="${p.id}" ${
String(p.id) === String(provinceId) ? 'selected' : ''
}>${p.name}</option>`
);
});
initChoices(province);
if (provinceId) {
loadCities(addressId, provinceId, cityId, districtId, villageId);
}
});
}
/* ===============================
* CITY
* =============================== */
function loadCities(addressId, provinceId, cityId = null, districtId = null, villageId = null) {
const city = document.querySelector(
`.city-select[data-address-id="${addressId}"]`
);
if (!city) return;
destroyChoices(city);
fetch(`/addresses/cities/${provinceId}`)
.then(r => r.json())
.then(({
data
}) => {
city.innerHTML =
`<option value="">{{ __('addresses.select_city') }}</option>`;
data.forEach(c => {
city.insertAdjacentHTML(
'beforeend',
`<option value="${c.id}" ${
String(c.id) === String(cityId) ? 'selected' : ''
}>${c.name}</option>`
);
});
initChoices(city);
if (cityId) {
loadDistricts(addressId, cityId, districtId, villageId);
}
});
}
/* ===============================
* DISTRICT
* =============================== */
function loadDistricts(addressId, cityId, districtId = null, villageId = null) {
const district = document.querySelector(
`.district-select[data-address-id="${addressId}"]`
);
if (!district) return;
destroyChoices(district);
fetch(`/addresses/districts/${cityId}`)
.then(r => r.json())
.then(({
data
}) => {
district.innerHTML =
`<option value="">{{ __('addresses.select_district') }}</option>`;
data.forEach(d => {
district.insertAdjacentHTML(
'beforeend',
`<option value="${d.id}" ${
String(d.id) === String(districtId) ? 'selected' : ''
}>${d.name}</option>`
);
});
initChoices(district);
if (districtId) {
loadVillages(addressId, districtId, villageId);
}
});
}
/* ===============================
* VILLAGE
* =============================== */
function loadVillages(addressId, districtId, villageId = null) {
const village = document.querySelector(
`.village-select[data-address-id="${addressId}"]`
);
if (!village) return;
destroyChoices(village);
fetch(`/addresses/villages/${districtId}`)
.then(r => r.json())
.then(({
data
}) => {
village.innerHTML =
`<option value="">{{ __('addresses.select_village') }}</option>`;
data.forEach(v => {
village.insertAdjacentHTML(
'beforeend',
`<option value="${v.id}" ${
String(v.id) === String(villageId) ? 'selected' : ''
}>${v.name}</option>`
);
});
initChoices(village);
});
}
/* ===============================
* CHANGE HANDLERS
* =============================== */
document.addEventListener('change', e => {
const el = e.target;
const addressId = el.dataset.addressId;
if (!addressId) return;
const city = document.querySelector(
`.city-select[data-address-id="${addressId}"]`
);
const district = document.querySelector(
`.district-select[data-address-id="${addressId}"]`
);
const village = document.querySelector(
`.village-select[data-address-id="${addressId}"]`
);
// 🔄 PROVINCE CHANGED
if (el.classList.contains('province-select')) {
resetSelect(city, "{{ __('addresses.select_city') }}");
resetSelect(district, "{{ __('addresses.select_district') }}");
resetSelect(village, "{{ __('addresses.select_village') }}");
if (el.value) {
loadCities(addressId, el.value);
}
}
// 🔄 CITY CHANGED
if (el.classList.contains('city-select')) {
resetSelect(district, "{{ __('addresses.select_district') }}");
resetSelect(village, "{{ __('addresses.select_village') }}");
if (el.value) {
loadDistricts(addressId, el.value);
}
}
// 🔄 DISTRICT CHANGED
if (el.classList.contains('district-select')) {
resetSelect(village, "{{ __('addresses.select_village') }}");
if (el.value) {
loadVillages(addressId, el.value);
}
}
});
function resetSelect(el, placeholder) {
if (!el) return;
// Destroy choices kalau ada
if (el._choices) {
el._choices.destroy();
el._choices = null;
}
// Reset option
el.innerHTML = `<option value="">${placeholder}</option>`;
// 🔥 JANGAN init Choices kalau belum ada
if (typeof window.Choices === 'undefined') return;
el._choices = new Choices(el, {
searchEnabled: true,
shouldSort: false,
itemSelectText: ''
});
}
/* ===============================
* NEW ADDRESS MODAL
* =============================== */
const newAddressModal = document.getElementById('newAddressModal');
if (newAddressModal) {
newAddressModal.addEventListener('show.bs.modal', () => {
loadNewProvinces();
});
}
function loadNewProvinces() {
const province = document.querySelector('.new-province-select');
if (!province) return;
destroyChoices(province);
fetch('/addresses/provinces')
.then(r => r.json())
.then(({
data
}) => {
province.innerHTML = `<option value="">{{ __('addresses.select_province') }}</option>`;
data.forEach(p => {
province.insertAdjacentHTML('beforeend',
`<option value="${p.id}">${p.name}</option>`);
});
initChoices(province);
});
}
function loadNewCities(provinceId) {
const city = document.querySelector('.new-city-select');
if (!city) return;
destroyChoices(city);
fetch(`/addresses/cities/${provinceId}`)
.then(r => r.json())
.then(({
data
}) => {
city.innerHTML = `<option value="">{{ __('addresses.select_city') }}</option>`;
data.forEach(c => {
city.insertAdjacentHTML('beforeend',
`<option value="${c.id}">${c.name}</option>`);
});
initChoices(city);
});
}
function loadNewDistricts(cityId) {
const district = document.querySelector('.new-district-select');
if (!district) return;
destroyChoices(district);
fetch(`/addresses/districts/${cityId}`)
.then(r => r.json())
.then(({
data
}) => {
district.innerHTML = `<option value="">{{ __('addresses.select_district') }}</option>`;
data.forEach(d => {
district.insertAdjacentHTML('beforeend',
`<option value="${d.id}">${d.name}</option>`);
});
initChoices(district);
});
}
function loadNewVillages(districtId) {
const village = document.querySelector('.new-village-select');
if (!village) return;
destroyChoices(village);
fetch(`/addresses/villages/${districtId}`)
.then(r => r.json())
.then(({
data
}) => {
village.innerHTML = `<option value="">{{ __('addresses.select_village') }}</option>`;
data.forEach(v => {
village.insertAdjacentHTML('beforeend',
`<option value="${v.id}">${v.name}</option>`);
});
initChoices(village);
});
}
// New address form change handlers
document.addEventListener('change', e => {
const el = e.target;
// 🔄 PROVINCE CHANGED
if (el.classList.contains('new-province-select')) {
resetSelect(document.querySelector('.new-city-select'),
"{{ __('addresses.select_city') }}");
resetSelect(document.querySelector('.new-district-select'),
"{{ __('addresses.select_district') }}");
resetSelect(document.querySelector('.new-village-select'),
"{{ __('addresses.select_village') }}");
if (el.value) {
loadNewCities(el.value);
}
}
// 🔄 CITY CHANGED
if (el.classList.contains('new-city-select')) {
resetSelect(document.querySelector('.new-district-select'),
"{{ __('addresses.select_district') }}");
resetSelect(document.querySelector('.new-village-select'),
"{{ __('addresses.select_village') }}");
if (el.value) {
loadNewDistricts(el.value);
}
}
// 🔄 DISTRICT CHANGED
if (el.classList.contains('new-district-select')) {
resetSelect(document.querySelector('.new-village-select'),
"{{ __('addresses.select_village') }}");
if (el.value) {
loadNewVillages(el.value);
}
}
});
/* ===============================
* DELETE ADDRESS
* =============================== */
let deleteAddressId = null;
let deleteModalInstance = null;
document.addEventListener('click', e => {
const deleteBtn = e.target.closest('.delete-address-btn');
if (!deleteBtn) return;
e.preventDefault(); // Prevent any default action
e.stopPropagation(); // Stop event propagation
deleteAddressId = deleteBtn.dataset.addressId;
const addressLabel = deleteBtn.dataset.addressLabel;
console.log('Delete button clicked:', {
deleteAddressId,
addressLabel
});
// Initialize modal if not already done
if (!deleteModalInstance) {
console.log('Initializing modal...');
console.log('Bootstrap available:', typeof bootstrap !== 'undefined');
console.log('Bootstrap.Modal available:', typeof bootstrap !== 'undefined' && bootstrap.Modal);
const deleteModal = document.getElementById('deleteAddressModal');
console.log('Modal element found:', !!deleteModal);
if (deleteModal && typeof bootstrap !== 'undefined' && bootstrap.Modal) {
deleteModalInstance = new bootstrap.Modal(deleteModal);
console.log('Modal instance created successfully');
} else {
console.error('Modal initialization failed:', {
modalExists: !!deleteModal,
bootstrapExists: typeof bootstrap !== 'undefined',
modalClassExists: typeof bootstrap !== 'undefined' && bootstrap.Modal
});
}
}
// Set address label in modal
const labelElement = document.getElementById('deleteAddressLabel');
if (labelElement) {
labelElement.textContent = addressLabel;
}
// Show modal
if (deleteModalInstance) {
console.log('Showing delete modal');
deleteModalInstance.show();
} else {
console.error('Modal instance not available');
// Fallback to confirm dialog
if (confirm(`{{ __('addresses.confirm_delete_address') }}\n${addressLabel}`)) {
deleteAddress(deleteAddressId);
}
}
});
// Handle confirm delete button click
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
console.log('Confirm delete clicked:', deleteAddressId);
if (deleteAddressId) {
deleteAddress(deleteAddressId);
if (deleteModalInstance) {
deleteModalInstance.hide();
}
}
});
function deleteAddress(addressId) {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
fetch(`/addresses/${addressId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
console.log(result);
if (result.success) {
// Show success message and reload page
showNotification(result.message, 'success');
setTimeout(() => {
reloadAddresses();
}, 1000);
} else {
// Show error message
showNotification(result.message || '{{ __('addresses.error_saving_address') }}',
'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('{{ __('addresses.error_saving_address') }}', 'error');
})
.finally(() => {
deleteAddressId = null;
});
}
/* ===============================
* FORM SUBMISSION
* =============================== */
document.addEventListener('submit', e => {
const form = e.target;
if (!form.classList.contains('address-form') && !form.classList.contains(
'new-address-form')) return;
e.preventDefault();
const formData = new FormData(form);
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
// Show loading state
submitBtn.disabled = true;
submitBtn.textContent = '{{ __('addresses.saving') }}...';
// Determine if this is a new address or edit form
const isNewAddress = form.classList.contains('new-address-form');
const addressId = isNewAddress ? null : form.querySelector('[data-address-id]').dataset
.addressId;
// Prepare form data for AJAX submission
const submitData = {};
// Get CSRF token
const tokenInput = form.querySelector('input[name="_token"]');
if (tokenInput) submitData._token = tokenInput.value;
// Get Name
const nameInput = isNewAddress ?
form.querySelector('#new-name') : form.querySelector('.edit-name');
if (nameInput) submitData.name = nameInput.value;
// Get Phone
const phoneInput = isNewAddress ?
form.querySelector('.new-phone') : form.querySelector('.edit-phone');
if (phoneInput) submitData.phone = phoneInput.value;
// Get Label
const labelInput = isNewAddress ?
form.querySelector('#new-label') : form.querySelector('.edit-label');
if (labelInput) submitData.label = labelInput.value;
// Get province ID
const provinceSelect = isNewAddress ?
form.querySelector('.new-province-select') : form.querySelector('.province-select');
if (provinceSelect) submitData.province_id = provinceSelect.value;
// Get city ID
const citySelect = isNewAddress ?
form.querySelector('.new-city-select') : form.querySelector('.city-select');
if (citySelect) submitData.city_id = citySelect.value;
// Get district ID
const districtSelect = isNewAddress ?
form.querySelector('.new-district-select') : form.querySelector('.district-select');
if (districtSelect) submitData.district_id = districtSelect.value;
// Get subdistrict ID
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 latitude
const latitudeInput = isNewAddress ?
form.querySelector('.new-latitude') : form.querySelector('.latitude');
if (latitudeInput) submitData.latitude = latitudeInput.value;
// Get longitude
const longitudeInput = isNewAddress ?
form.querySelector('.new-longitude') : form.querySelector('.longitude');
if (longitudeInput) submitData.longitude = longitudeInput.value;
// Get address
const addressInput = isNewAddress ?
form.querySelector('#new-address') : form.querySelector('.address-input');
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) {
submitData._method = 'PUT';
}
fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams(submitData).toString()
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Show success message in form
const successElement = form.querySelector(isNewAddress ?
'.new-address-form-success' : '.address-form-success');
const errorElement = form.querySelector(isNewAddress ?
'.new-address-form-message' : '.address-form-message');
if (successElement) {
successElement.textContent = result.message;
successElement.classList.remove('d-none');
}
if (errorElement) {
errorElement.classList.add('d-none');
}
// Handle success based on form type
if (isNewAddress) {
// Close modal and reload page
setTimeout(() => {
// Try multiple methods to close the modal
try {
// Method 1: Using Bootstrap 5 API if available
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
const modal = bootstrap.Modal.getInstance(newAddressModal);
if (modal) modal.hide();
} else {
// Method 2: Using jQuery if available
if (typeof $ !== 'undefined') {
$(newAddressModal).modal('hide');
} else {
// Method 3: Using native DOM manipulation
newAddressModal.style.display = 'none';
document.body.classList.remove('modal-open');
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) backdrop.remove();
}
}
} catch (error) {
console.warn('Error closing modal:', error);
// Fallback: hide modal manually
newAddressModal.style.display = 'none';
}
// Refresh page after successful submission
setTimeout(() => {
window.location.reload();
}, 500);
}, 1500);
} else {
// Close the edit form and show preview after delay
setTimeout(() => {
const previewElement = document.getElementById(
`primaryAddressPreview${addressId}`);
const editElement = document.getElementById(
`primaryAddressEdit${addressId}`);
if (previewElement && editElement) {
previewElement.classList.add('show');
editElement.classList.remove('show');
}
// Reload page to show updated data
reloadAddresses();
}, 2000);
}
} else {
// Show error message in form
const errorElement = form.querySelector(isNewAddress ?
'.new-address-form-message' : '.address-form-message');
const successElement = form.querySelector(isNewAddress ?
'.new-address-form-success' : '.address-form-success');
if (errorElement) {
errorElement.textContent = result.message ||
'{{ __('addresses.error_saving_address') }}';
errorElement.classList.remove('d-none');
}
if (successElement) {
successElement.classList.add('d-none');
}
}
})
.catch(error => {
console.error('Error:', error);
// Show error message in form
const errorElement = form.querySelector(isNewAddress ?
'.new-address-form-message' : '.address-form-message');
const successElement = form.querySelector(isNewAddress ?
'.new-address-form-success' : '.address-form-success');
if (errorElement) {
errorElement.textContent = '{{ __('addresses.error_saving_address') }}';
errorElement.classList.remove('d-none');
}
if (successElement) {
successElement.classList.add('d-none');
}
})
.finally(() => {
// Restore button state
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
// Toast notification helper
function showToast(message, type = 'info') {
console.log('Showing toast:', message, type);
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '1055';
document.body.appendChild(toastContainer);
}
const toastId = 'toast-' + Date.now();
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'primary'} border-0" role="alert">
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
// Show toast using Bootstrap if available, otherwise fallback to simple alert
const toastElement = document.getElementById(toastId);
if (typeof bootstrap !== 'undefined' && bootstrap.Toast) {
try {
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 notification helper (deprecated - use showNotification instead)
function showToast(message, type = 'info') {
console.warn('showToast is deprecated, use showNotification instead');
showNotification(message, type);
}
/* ===============================
* HELPERS
* =============================== */
function destroyChoices(el) {
if (el._choices) {
el._choices.destroy();
el._choices = null;
}
}
function initChoices(el) {
if (!window.Choices) return;
el._choices = new Choices(el, {
searchEnabled: true,
shouldSort: false,
itemSelectText: '',
noResultsText: 'No results found'
});
}
});
</script>
@endsection