feat: Add Google Places autocomplete for address search
- Add address search input field with autocomplete - Integrate Google Places API to provide address suggestions - Parse selected address components and auto-populate form fields - Address fields initially hidden, show when address is selected - Add clear button to reset address selection - Support South Africa locations with country restriction - Add Google Places API key config to services.php - Hide/show address fields based on selection and validation errors
This commit is contained in:
@@ -59,4 +59,8 @@ return [
|
|||||||
'redirect' => env('GOOGLE_REDIRECT_URI', '/auth/google/callback'),
|
'redirect' => env('GOOGLE_REDIRECT_URI', '/auth/google/callback'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'google_places' => [
|
||||||
|
'api_key' => env('GOOGLE_PLACES_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group input,
|
.form-group input,
|
||||||
.form-group textarea {
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@@ -58,12 +59,51 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus,
|
.form-group input:focus,
|
||||||
.form-group textarea:focus {
|
.form-group textarea:focus,
|
||||||
|
.form-group select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 3px rgba(45, 80, 71, 0.1);
|
box-shadow: 0 0 0 3px rgba(45, 80, 71, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Google Places Autocomplete styling */
|
||||||
|
.pac-container {
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pac-item {
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pac-item:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-search-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 32px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-clear-btn.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.order-summary {
|
.order-summary {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
@@ -211,42 +251,50 @@
|
|||||||
<input type="tel" id="customer_phone" name="customer_phone" value="{{ old('customer_phone') }}" required>
|
<input type="tel" id="customer_phone" name="customer_phone" value="{{ old('customer_phone') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="address-search-group">
|
||||||
<label for="shipping_street_address">Street Address</label>
|
<label for="address_search">Delivery Address</label>
|
||||||
<input type="text" id="shipping_street_address" name="shipping_street_address" placeholder="e.g., 123 Main Street" value="{{ old('shipping_street_address') }}" required>
|
<input type="text" id="address_search" placeholder="Start typing your address..." autocomplete="off">
|
||||||
|
<button type="button" class="address-clear-btn" id="address_clear_btn">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div id="address_fields_group" style="display: none;">
|
||||||
<label for="shipping_local_area">Suburb/Local Area</label>
|
<div class="form-group">
|
||||||
<input type="text" id="shipping_local_area" name="shipping_local_area" placeholder="e.g., Menlyn" value="{{ old('shipping_local_area') }}" required>
|
<label for="shipping_street_address">Street Address</label>
|
||||||
</div>
|
<input type="text" id="shipping_street_address" name="shipping_street_address" value="{{ old('shipping_street_address') }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_city">City</label>
|
<label for="shipping_local_area">Suburb/Local Area</label>
|
||||||
<input type="text" id="shipping_city" name="shipping_city" placeholder="e.g., Pretoria" value="{{ old('shipping_city') }}" required>
|
<input type="text" id="shipping_local_area" name="shipping_local_area" value="{{ old('shipping_local_area') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_zone">Province/Zone</label>
|
<label for="shipping_city">City</label>
|
||||||
<input type="text" id="shipping_zone" name="shipping_zone" placeholder="e.g., Gauteng" value="{{ old('shipping_zone') }}" required>
|
<input type="text" id="shipping_city" name="shipping_city" value="{{ old('shipping_city') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_postcode">Postal Code</label>
|
<label for="shipping_zone">Province/Zone</label>
|
||||||
<input type="text" id="shipping_postcode" name="shipping_postcode" placeholder="e.g., 0181" value="{{ old('shipping_postcode') }}" required>
|
<input type="text" id="shipping_zone" name="shipping_zone" value="{{ old('shipping_zone') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_country">Country</label>
|
<label for="shipping_postcode">Postal Code</label>
|
||||||
<input type="text" id="shipping_country" name="shipping_country" value="ZA" readonly>
|
<input type="text" id="shipping_postcode" name="shipping_postcode" value="{{ old('shipping_postcode') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_type">Address Type</label>
|
<label for="shipping_country">Country</label>
|
||||||
<select id="shipping_type" name="shipping_type" required>
|
<input type="text" id="shipping_country" name="shipping_country" value="ZA" readonly>
|
||||||
<option value="residential" {{ old('shipping_type') === 'residential' ? 'selected' : '' }}>Residential</option>
|
</div>
|
||||||
<option value="business" {{ old('shipping_type') === 'business' ? 'selected' : '' }}>Business</option>
|
|
||||||
</select>
|
<div class="form-group">
|
||||||
|
<label for="shipping_type">Address Type</label>
|
||||||
|
<select id="shipping_type" name="shipping_type" required>
|
||||||
|
<option value="residential" {{ old('shipping_type') === 'residential' ? 'selected' : '' }}>Residential</option>
|
||||||
|
<option value="business" {{ old('shipping_type') === 'business' ? 'selected' : '' }}>Business</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -320,4 +368,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://maps.googleapis.com/maps/api/js?key={{ config('services.google_places.api_key') }}&libraries=places"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const addressSearchInput = document.getElementById('address_search');
|
||||||
|
const addressFieldsGroup = document.getElementById('address_fields_group');
|
||||||
|
const clearBtn = document.getElementById('address_clear_btn');
|
||||||
|
|
||||||
|
if (!addressSearchInput) return;
|
||||||
|
|
||||||
|
// Initialize Google Places Autocomplete
|
||||||
|
const autocomplete = new google.maps.places.Autocomplete(addressSearchInput, {
|
||||||
|
componentRestrictions: { country: 'za' },
|
||||||
|
types: ['geocode']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent form submission on Enter when autocomplete is open
|
||||||
|
addressSearchInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && document.querySelector('.pac-container:not([style*="display: none"])')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When place is selected
|
||||||
|
autocomplete.addListener('place_changed', function() {
|
||||||
|
const place = autocomplete.getPlace();
|
||||||
|
|
||||||
|
if (!place.geometry) {
|
||||||
|
console.error('Place has no geometry');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse address components
|
||||||
|
const addressComponents = {};
|
||||||
|
place.address_components.forEach(component => {
|
||||||
|
addressComponents[component.types[0]] = component.long_name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate form fields
|
||||||
|
document.getElementById('shipping_street_address').value =
|
||||||
|
(addressComponents['street_number'] ? addressComponents['street_number'] + ' ' : '') +
|
||||||
|
(addressComponents['route'] || '');
|
||||||
|
|
||||||
|
document.getElementById('shipping_local_area').value =
|
||||||
|
addressComponents['sublocality'] || addressComponents['sublocality_level_1'] || '';
|
||||||
|
|
||||||
|
document.getElementById('shipping_city').value =
|
||||||
|
addressComponents['locality'] || addressComponents['administrative_area_level_2'] || '';
|
||||||
|
|
||||||
|
document.getElementById('shipping_zone').value =
|
||||||
|
addressComponents['administrative_area_level_1'] || '';
|
||||||
|
|
||||||
|
document.getElementById('shipping_postcode').value =
|
||||||
|
addressComponents['postal_code'] || '';
|
||||||
|
|
||||||
|
document.getElementById('shipping_country').value =
|
||||||
|
addressComponents['country'] || 'ZA';
|
||||||
|
|
||||||
|
// Show address fields group
|
||||||
|
addressFieldsGroup.style.display = 'block';
|
||||||
|
|
||||||
|
// Show clear button
|
||||||
|
clearBtn.classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear button functionality
|
||||||
|
clearBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
addressSearchInput.value = '';
|
||||||
|
addressSearchInput.focus();
|
||||||
|
clearBtn.classList.remove('show');
|
||||||
|
addressFieldsGroup.style.display = 'none';
|
||||||
|
|
||||||
|
// Clear form fields
|
||||||
|
document.getElementById('shipping_street_address').value = '';
|
||||||
|
document.getElementById('shipping_local_area').value = '';
|
||||||
|
document.getElementById('shipping_city').value = '';
|
||||||
|
document.getElementById('shipping_zone').value = '';
|
||||||
|
document.getElementById('shipping_postcode').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show fields if form has old values (validation error)
|
||||||
|
const hasOldValues =
|
||||||
|
document.getElementById('shipping_street_address').value ||
|
||||||
|
document.getElementById('shipping_city').value;
|
||||||
|
|
||||||
|
if (hasOldValues) {
|
||||||
|
addressFieldsGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-600">Packed At</p>
|
<p class="text-sm text-gray-600">Packed At</p>
|
||||||
<p class="text-lg font-semibold"><?php echo e($order->packing_completed_at->format('M d, H:i')); ?></p>
|
<p class="text-lg font-semibold"><?php echo e($order->packing_completed_at ? $order->packing_completed_at->format('M d, H:i') : 'Not packed'); ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!-- Read-Only State -->
|
||||||
|
<div class="bg-blue-50 border-2 border-blue-200 rounded-lg p-6 mb-8">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-2xl font-bold text-blue-900 mb-2">📋 Order Status: Read-Only</h2>
|
||||||
|
<p class="text-blue-800 mb-4">
|
||||||
|
This order is in the <strong><?php echo e(str_replace('_', ' ', ucfirst($order->status))); ?></strong> phase.
|
||||||
|
No actions are available at this stage.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($order->status === 'ready_to_ship'): ?>
|
||||||
|
<div class="bg-white border-l-4 border-blue-500 p-4 rounded">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<strong>Next Step:</strong> Move Trello card to "Ready to Ship" to trigger automatic shipment creation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php elseif($order->status === 'awaiting_collection'): ?>
|
||||||
|
<div class="bg-white border-l-4 border-blue-500 p-4 rounded">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<strong>Status:</strong> Parcel is awaiting collection from courier.
|
||||||
|
</p>
|
||||||
|
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($order->courier_waybill_id): ?>
|
||||||
|
<p class="text-gray-700 mt-2">
|
||||||
|
<strong>Waybill:</strong> <code class="bg-gray-100 px-2 py-1 rounded"><?php echo e($order->courier_waybill_id); ?></code>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php elseif($order->status === 'in_transit'): ?>
|
||||||
|
<div class="bg-white border-l-4 border-green-500 p-4 rounded">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<strong>Status:</strong> Parcel is in transit to the customer.
|
||||||
|
</p>
|
||||||
|
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($order->courier_tracking_number): ?>
|
||||||
|
<p class="text-gray-700 mt-2">
|
||||||
|
<strong>Tracking:</strong> <code class="bg-gray-100 px-2 py-1 rounded"><?php echo e($order->courier_tracking_number); ?></code>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="bg-white border-l-4 border-gray-500 p-4 rounded">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
The order is progressing through the fulfillment pipeline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php /**PATH /var/www/additional_design/resources/views/ops/actions/read-only.blade.php ENDPATH**/ ?>
|
||||||
@@ -210,8 +210,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shipping_address">Delivery Address</label>
|
<label for="shipping_street_address">Street Address</label>
|
||||||
<textarea id="shipping_address" name="shipping_address" required><?php echo e(old('shipping_address')); ?></textarea>
|
<input type="text" id="shipping_street_address" name="shipping_street_address" placeholder="e.g., 123 Main Street" value="<?php echo e(old('shipping_street_address')); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_local_area">Suburb/Local Area</label>
|
||||||
|
<input type="text" id="shipping_local_area" name="shipping_local_area" placeholder="e.g., Menlyn" value="<?php echo e(old('shipping_local_area')); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_city">City</label>
|
||||||
|
<input type="text" id="shipping_city" name="shipping_city" placeholder="e.g., Pretoria" value="<?php echo e(old('shipping_city')); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_zone">Province/Zone</label>
|
||||||
|
<input type="text" id="shipping_zone" name="shipping_zone" placeholder="e.g., Gauteng" value="<?php echo e(old('shipping_zone')); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_postcode">Postal Code</label>
|
||||||
|
<input type="text" id="shipping_postcode" name="shipping_postcode" placeholder="e.g., 0181" value="<?php echo e(old('shipping_postcode')); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_country">Country</label>
|
||||||
|
<input type="text" id="shipping_country" name="shipping_country" value="ZA" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shipping_type">Address Type</label>
|
||||||
|
<select id="shipping_type" name="shipping_type" required>
|
||||||
|
<option value="residential" <?php echo e(old('shipping_type') === 'residential' ? 'selected' : ''); ?>>Residential</option>
|
||||||
|
<option value="business" <?php echo e(old('shipping_type') === 'business' ? 'selected' : ''); ?>>Business</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user