1167 lines
54 KiB
PHP
1167 lines
54 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.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-4">
|
|
<div class="position-relative">
|
|
<label for="new-zip" class="form-label">{{ __('addresses.zip_code') }}</label>
|
|
<input type="text" class="form-control" id="new-zip" required>
|
|
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-8">
|
|
<div class="position-relative">
|
|
<label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
|
|
<input type="text" class="form-control" id="new-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-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.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
|
|
* =============================== */
|
|
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 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 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) {
|
|
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(() => {
|
|
const modal = bootstrap.Modal.getInstance(newAddressModal);
|
|
if (modal) modal.hide();
|
|
reloadAddresses();
|
|
}, 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
|