Files
Additional/storage/framework/views/963a42c38bd8e0fdfe0b4e765ea9f100.php
T
twotalesanimation 2a10f9af38 feat: Complete Shiplogic integration with mobile-optimized ops workflow
**Shiplogic API Integration:**
- Fixed API base URL configuration (removed /api suffix)
- Implemented comprehensive request/response logging for rates and shipments endpoints
- Fixed PDF fetching: API returns S3 URLs, now downloads actual PDFs from S3
- Added tests and mock API responses for local development (routes/shiplogic-mock.php)

**Courier Service Enhancements:**
- Added redownloadShipmentPdfs() public method for re-downloading corrupted PDFs
- Enhanced error logging with full request/response bodies for debugging
- Proper binary PDF storage using Laravel Storage facade
- URL and S3 download handling for Shiplogic API responses

**Workflow & Operations:**
- Changed to manual "Ready for Collection" button instead of automatic move
- Operators now: scan QR → apply labels → click "Ready for Collection" → moves to Awaiting Collection
- Removed duplicate PDF attachments to Trello (was adding twice from two listeners)
- Fixed NotifySlackOnShipmentCreated to only handle Slack notifications

**Mobile-Optimized Ops Page:**
- Removed QR code display from order detail page
- Implemented responsive single-column layout for mobile phones
- Large touch-friendly buttons (full width, increased padding)
- Bold typography for better readability on small screens
- Larger input fields and tracking number displays
- Clear step-by-step instructions for warehouse operators
- Re-download PDF button for damaged/corrupted labels

**New Features:**
- POST /ops/orders/{uuid}/ready-for-collection endpoint
- Re-download PDFs functionality accessible from awaiting_collection and in_transit states
- Full audit logging for all operations via ops interface
- Proper error handling and user feedback

**Testing:**
- Added ShipmentCreationTest with mock HTTP client
- Created comprehensive testing guide (SHIPLOGIC_TESTING.md)
- Mock API routes for local development without hitting live API
2026-01-03 16:13:20 +02:00

562 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php $__env->startSection('title', 'Checkout - Additional Design'); ?>
<?php $__env->startSection('styles'); ?>
<style>
h1 {
font-family: 'Abril Fatface', cursive;
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 2rem;
}
.checkout-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
margin: 2rem 0 3rem 0;
}
.checkout-form,
.order-summary {
border-radius: 20px;
}
.checkout-form h2,
.order-summary h2 {
font-family: 'Abril Fatface', cursive;
font-size: 1.5rem;
color: var(--primary-color);
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.form-group input,
.form-group textarea,
.form-group select,
.address-search-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus,
.address-search-group input: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;
}
.order-item {
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.order-item:last-child {
border-bottom: none;
}
.order-item-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.order-item-details {
font-size: 0.85rem;
color: #666;
margin-bottom: 0.5rem;
}
.order-item-price {
text-align: right;
font-weight: 600;
color: var(--primary-color);
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.summary-row.total {
font-weight: 600;
font-size: 1.2rem;
color: var(--primary-color);
border-top: 2px solid var(--primary-color);
border-bottom: none;
padding-top: 1.5rem;
margin-top: 1rem;
}
.btn-submit {
width: 100%;
margin-top: 2rem;
padding: 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.btn-submit:hover {
background-color: #1f3633;
}
.alert {
padding: 1rem;
border-radius: 4px;
margin-bottom: 2rem;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info-box {
background: var(--section-light-bg);
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
font-size: 0.9rem;
color: #666;
}
.back-link {
display: block;
text-align: center;
margin-top: 1.5rem;
color: var(--primary-color);
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.checkout-container {
grid-template-columns: 1fr;
gap: 2rem;
}
h1 {
font-size: 1.8rem;
}
}
</style>
<?php $__env->stopSection(); ?>
<?php $__env->startSection('content'); ?>
<div class="container">
<h1>Checkout</h1>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($errors->any()): ?>
<div class="alert alert-error">
<ul style="margin: 0; padding-left: 1.5rem;">
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__currentLoopData = $errors->all(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $error): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<li><?php echo e($error); ?></li>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</ul>
</div>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<div class="checkout-container">
<!-- Checkout Form -->
<form action="<?php echo e(route('order-process')); ?>" method="POST" class="checkout-form card">
<?php echo csrf_field(); ?>
<h2>Delivery Information</h2>
<div class="form-group">
<label for="customer_name">Full Name<span style="color: red;">*</span></label>
<input type="text" id="customer_name" name="customer_name" value="<?php echo e(old('customer_name', Auth::check() ? Auth::user()->name : '')); ?>" required>
</div>
<div class="form-group">
<label for="customer_email">Email Address<span style="color: red;">*</span></label>
<input type="email" id="customer_email" name="customer_email" value="<?php echo e(old('customer_email', Auth::check() ? Auth::user()->email : '')); ?>" required>
</div>
<div class="form-group">
<label for="customer_phone">Phone Number<span style="color: red;">*</span></label>
<input type="tel" id="customer_phone" name="customer_phone" value="<?php echo e(old('customer_phone')); ?>" required>
</div>
<div class="form-group">
<label for="shipping_type">Address Type <span style="color: red;">*</span></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" id="business_name_group" style="display: none;">
<label for="business_name">Business Name <span style="color: red;">*</span></label>
<input type="text" id="business_name" name="business_name" value="<?php echo e(old('business_name')); ?>" placeholder="Enter your business name">
</div>
<div class="address-search-group">
<label for="address_search">Delivery Address (Type to search)</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 id="address_fields_group">
<div class="form-group">
<label for="shipping_unit_number">Apartment / Unit / Building Number (Optional)</label>
<input type="text" id="shipping_unit_number" name="shipping_unit_number" placeholder="e.g. Apt 101, Unit B, Building 3" value="<?php echo e(old('shipping_unit_number')); ?>">
</div>
<div class="form-group">
<label for="shipping_street_address">Street Address <span style="color: red;">*</span></label>
<input type="text" id="shipping_street_address" name="shipping_street_address" value="<?php echo e(old('shipping_street_address')); ?>" required>
</div>
<div class="form-group">
<label for="shipping_local_area">Suburb<span style="color: red;">*</span></label>
<input type="text" id="shipping_local_area" name="shipping_local_area" value="<?php echo e(old('shipping_local_area')); ?>" required>
</div>
<div class="form-group">
<label for="shipping_city">City <span style="color: red;">*</span></label>
<input type="text" id="shipping_city" name="shipping_city" value="<?php echo e(old('shipping_city')); ?>" required>
</div>
<div class="form-group">
<label for="shipping_zone">Province<span style="color: red;">*</span></label>
<input type="text" id="shipping_zone" name="shipping_zone" value="<?php echo e(old('shipping_zone')); ?>" required>
</div>
<div class="form-group">
<label for="shipping_postcode">Postal Code <span style="color: red;">*</span></label>
<input type="text" id="shipping_postcode" name="shipping_postcode" 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>
<div class="form-group">
<label for="notes">Order Notes (Optional)</label>
<textarea id="notes" name="notes" placeholder="Any special instructions or notes for your order..."><?php echo e(old('notes')); ?></textarea>
</div>
<div class="info-box">
<strong>Note:</strong> Payment will be processed securely through Yoco. You'll be redirected to complete your card payment after confirming this order.
</div>
<button type="submit" class="btn">Complete Order & Proceed to Payment</button>
</form>
<!-- Order Summary -->
<div class="order-summary card">
<h2>Order Summary</h2>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__currentLoopData = $items; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<div class="order-item">
<div class="order-item-name"><?php echo e($item['product']->name); ?><?php echo e($item['is_sample'] ? ' - Sample' : ''); ?></div>
<?php
$stockCost = 0;
$stockUnit = $item['type'] === 'wallpaper' ? '/m' : '/m²';
if (!$item['is_sample'] && $item['stock']) {
$stockCost = $item['type'] === 'wallpaper' ? $item['stock']->cost_per_meter : $item['stock']->cost_per_m2;
}
?>
<div class="order-item-details">
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($item['is_sample']): ?>
<strong>Sample - Fixed Price:</strong> R<?php echo e(number_format(\App\Services\ShippingService::getSampleCost(), 2)); ?><br>
<?php elseif($item['stock']): ?>
<strong><?php echo e($item['stock']->name); ?>:</strong> R<?php echo e(number_format($stockCost, 2)); ?><?php echo e($stockUnit); ?><br>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if(!$item['is_sample']): ?>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($item['type'] === 'wallpaper'): ?>
<strong>Length:</strong> <?php echo e($item['length']); ?>m
<?php elseif($item['type'] === 'mural'): ?>
<strong>Dimensions:</strong> <?php echo e($item['width']); ?>m × <?php echo e($item['height']); ?>m (<?php echo e(number_format($item['width'] * $item['height'], 2)); ?>m²)
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($item['quantity'] > 1): ?>
<br><strong>Quantity:</strong> <?php echo e($item['quantity']); ?>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</div>
<div class="order-item-price">R<?php echo e(number_format($item['subtotal'], 2)); ?></div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<div class="summary-row">
<span>Subtotal:</span>
<span>R<?php echo e(number_format($total, 2)); ?></span>
</div>
<div class="summary-row">
<span><?php echo e($shippingLabel); ?>:</span>
<span>R<?php echo e(number_format($shippingFee, 2)); ?></span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span>R<?php echo e(number_format($grandTotal, 2)); ?></span>
</div>
<a href="<?php echo e(route('cart')); ?>" class="back-link">← Back to Cart</a>
</div>
</div>
</div>
<?php
$apiKey = config('services.google_places.api_key');
?>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if(!$apiKey): ?>
<div style="background: #fff3cd; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #856404;">
<strong>⚠️ Configuration Issue:</strong> Google Places API key is not configured. Please add <code>GOOGLE_PLACES_API_KEY</code> to your .env file.
</div>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<script>
// console.log('Checkout page loaded');
// console.log('API Key configured:', <?php echo e($apiKey ? 'true' : 'false'); ?>);
// <?php if($apiKey): ?>
// console.log('API Key length:', <?php echo e(strlen($apiKey)); ?>);
// <?php endif; ?>
</script>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php if($apiKey): ?>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=<?php echo e($apiKey); ?>&loading=async&libraries=places&callback=initializeAddressAutocomplete"></script>
<!-- <script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "<?php echo e($apiKey); ?>",
v: "weekly",
}); -->
</script>
<script>
// Handle shipping type change
document.getElementById('shipping_type').addEventListener('change', function() {
const businessNameGroup = document.getElementById('business_name_group');
const businessNameInput = document.getElementById('business_name');
if (this.value === 'business') {
businessNameGroup.style.display = 'block';
businessNameInput.setAttribute('required', 'required');
} else {
businessNameGroup.style.display = 'none';
businessNameInput.removeAttribute('required');
businessNameInput.value = '';
}
});
// Check initial state on page load
document.addEventListener('DOMContentLoaded', function() {
const shippingType = document.getElementById('shipping_type');
if (shippingType && shippingType.value === 'business') {
document.getElementById('business_name_group').style.display = 'block';
}
});
function initializeAddressAutocomplete() {
console.log('✓ Google Maps API loaded');
const addressSearchInput = document.getElementById('address_search');
const clearBtn = document.getElementById('address_clear_btn');
if (!addressSearchInput) {
console.error('Address search input not found');
return;
}
try {
console.log('Initializing Google Places Autocomplete...');
// Using modern Autocomplete with new API
const autocomplete = new google.maps.places.Autocomplete(addressSearchInput, {
componentRestrictions: { country: 'za' },
types: ['geocode']
});
console.log('✓ Google Places Autocomplete initialized');
// Prevent form submission on Enter when autocomplete dropdown is open
addressSearchInput.addEventListener('keydown', function(e) {
const pacContainer = document.querySelector('.pac-container:not([style*="display: none"])');
if (e.key === 'Enter' && pacContainer) {
e.preventDefault();
}
});
// When place is selected
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (!place.geometry) {
console.warn('Selected place has no geometry');
return;
}
console.log('✓ Address selected:', place.formatted_address);
// Parse address components
const addressComponents = {};
place.address_components.forEach(component => {
addressComponents[component.types[0]] = component.long_name;
});
// Debug: log all available components
console.log('Address components available:', Object.keys(addressComponents));
// Populate form fields
document.getElementById('shipping_street_address').value =
(addressComponents['street_number'] ? addressComponents['street_number'] + ' ' : '') +
(addressComponents['route'] || '');
document.getElementById('shipping_local_area').value =
addressComponents['political'] ||
addressComponents['sublocality'] ||
addressComponents['sublocality_level_1'] ||
addressComponents['sublocality_level_2'] || '';
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 clear button
clearBtn.classList.add('show');
});
// Clear button functionality
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
addressSearchInput.value = '';
addressSearchInput.focus();
clearBtn.classList.remove('show');
// Clear form fields
document.getElementById('shipping_street_address').value = '';
document.getElementById('shipping_unit_number').value = '';
document.getElementById('shipping_local_area').value = '';
document.getElementById('shipping_city').value = '';
document.getElementById('shipping_zone').value = '';
document.getElementById('shipping_postcode').value = '';
});
} catch (error) {
console.error('✗ Error initializing autocomplete:', error);
showAddressFieldsManually();
}
}
function showAddressFieldsManually() {
const addressSearchInput = document.getElementById('address_search');
if (addressSearchInput) {
addressSearchInput.style.display = 'none';
document.querySelector('label[for="address_search"]').style.display = 'none';
document.getElementById('address_clear_btn').style.display = 'none';
const fieldsGroup = document.getElementById('address_fields_group');
if (fieldsGroup) {
const notice = document.createElement('div');
notice.style.cssText = 'background: #f0f0f0; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; color: #666;';
notice.innerHTML = '<strong>Note:</strong> Address search is not available. Please fill in your address details manually below.';
fieldsGroup.parentNode.insertBefore(notice, fieldsGroup);
}
}
}
// If Google API doesn't load after 5 seconds, show fallback
setTimeout(() => {
if (typeof google === 'undefined') {
console.warn('Google Maps API failed to load');
showAddressFieldsManually();
}
}, 5000);
</script>
<?php endif; ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/additional_design/resources/views/checkout.blade.php ENDPATH**/ ?>