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:
twotalesanimation
2026-01-03 00:15:30 +02:00
parent dd950b5aba
commit b8cc8bd421
5 changed files with 260 additions and 34 deletions
+4
View File
@@ -59,4 +59,8 @@ return [
'redirect' => env('GOOGLE_REDIRECT_URI', '/auth/google/callback'),
],
'google_places' => [
'api_key' => env('GOOGLE_PLACES_API_KEY'),
],
];
+171 -31
View File
@@ -43,7 +43,8 @@
}
.form-group input,
.form-group textarea {
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
@@ -58,12 +59,51 @@
}
.form-group input:focus,
.form-group textarea:focus {
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
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 {
height: fit-content;
}
@@ -211,42 +251,50 @@
<input type="tel" id="customer_phone" name="customer_phone" value="{{ old('customer_phone') }}" required>
</div>
<div class="form-group">
<label for="shipping_street_address">Street 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>
<div class="address-search-group">
<label for="address_search">Delivery Address</label>
<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 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="{{ old('shipping_local_area') }}" required>
</div>
<div id="address_fields_group" style="display: none;">
<div class="form-group">
<label for="shipping_street_address">Street Address</label>
<input type="text" id="shipping_street_address" name="shipping_street_address" value="{{ old('shipping_street_address') }}" 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="{{ old('shipping_city') }}" 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" value="{{ old('shipping_local_area') }}" 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="{{ old('shipping_zone') }}" required>
</div>
<div class="form-group">
<label for="shipping_city">City</label>
<input type="text" id="shipping_city" name="shipping_city" value="{{ old('shipping_city') }}" 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="{{ old('shipping_postcode') }}" required>
</div>
<div class="form-group">
<label for="shipping_zone">Province/Zone</label>
<input type="text" id="shipping_zone" name="shipping_zone" value="{{ old('shipping_zone') }}" 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_postcode">Postal Code</label>
<input type="text" id="shipping_postcode" name="shipping_postcode" value="{{ old('shipping_postcode') }}" required>
</div>
<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 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" {{ old('shipping_type') === 'residential' ? 'selected' : '' }}>Residential</option>
<option value="business" {{ old('shipping_type') === 'business' ? 'selected' : '' }}>Business</option>
</select>
</div>
</div>
<div class="form-group">
@@ -320,4 +368,96 @@
</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
@@ -109,7 +109,7 @@
</div>
<div>
<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>
@@ -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 class="form-group">
<label for="shipping_address">Delivery Address</label>
<textarea id="shipping_address" name="shipping_address" required><?php echo e(old('shipping_address')); ?></textarea>
<label for="shipping_street_address">Street Address</label>
<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 class="form-group">