class McLeodEditor {
constructor(data) {
this.data = data;
this.commoditySuggestions = [];
// Configure API paths
this.apiPaths = {
// Base path for API requests - adjust this according to your server setup
base: '', // Empty string for same directory, or '/api' for a subdirectory
// Individual API endpoints
searchLocation: 'search-location.php',
createLocation: 'create-location.php',
commodityLookup: 'commodity-lookup.php',
sendToMcleod: 'send-to-mcleod.php',
uploadPdf: 'upload-pdf-to-mcleod.php'
};
// Extract broker name from driver summary immediately at initialization
this.extractBrokerFromDriverSummary();
this.render();
}
// New method to extract broker name from driver summary at initialization
extractBrokerFromDriverSummary() {
// Only extract if we don't already have a broker name
if (!this.data.broker_name) {
const driverContent = document.getElementById('driverContent');
if (driverContent) {
const brokerMatch = driverContent.innerText.match(/BROKER:\s*([^\n]+)/);
if (brokerMatch && brokerMatch[1]) {
// Normalize broker name
this.data.broker_name = this.normalizeBrokerName(brokerMatch[1].trim());
console.log("Extracted broker name at initialization:", this.data.broker_name);
}
}
} else {
// Normalize existing broker name
this.data.broker_name = this.normalizeBrokerName(this.data.broker_name);
console.log("Normalized existing broker name:", this.data.broker_name);
}
}
getApiUrl(endpoint, params = {}) {
// Start with the base path and endpoint
let url = `${this.apiPaths.base}/${this.apiPaths[endpoint]}`;
// Add parameters if any
if (Object.keys(params).length > 0) {
const queryParams = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
url += `?${queryParams}`;
}
return url;
}
// Normalize broker name - removing trailing commas, periods, and fixing company suffixes
normalizeBrokerName(name) {
if (!name) return '';
// Remove trailing commas and periods
let normalized = name.trim();
normalized = normalized.replace(/[,\.]+$/, '');
// Replace ", Inc" with " Inc" (remove comma before Inc, Corp, LLC, etc.)
normalized = normalized.replace(/,\s+(Inc|LLC|Corp|Corporation|Company)/, ' $1');
// Convert to uppercase to match McLeod API expectations
normalized = normalized.toUpperCase();
return normalized;
}
// Add this new method for aggressive stop type fixing
fixStopTypes() {
if (!this.data.stops || this.data.stops.length === 0) {
console.log("No stops to fix");
return;
}
// Force the first stop to be pickup
if (this.data.stops[0]) {
this.data.stops[0].stop_type = 'PU';
console.log('Force-set first stop to PU');
}
// Force all middle stops to be delivery
for (let i = 1; i < this.data.stops.length; i++) {
this.data.stops[i].stop_type = 'SO';
console.log(`Force-set stop ${i+1} to SO`);
}
// Extra enforcement for the last stop
if (this.data.stops.length > 1) {
const lastIndex = this.data.stops.length - 1;
this.data.stops[lastIndex].stop_type = 'SO';
console.log('Force-set last stop to SO (extra enforcement)');
}
// Log the stop types for debugging
console.log('Stop types after fixing:', this.data.stops.map(stop => stop.stop_type));
}
searchLocation(stopIndex, searchTerm) {
if (!searchTerm) return;
// Show searching indicator
const locationIdInput = document.querySelector(`input[data-stop="${stopIndex}"][data-field="location_id"]`);
if (locationIdInput) {
locationIdInput.classList.add('bg-yellow-100');
locationIdInput.setAttribute('placeholder', 'Searching...');
}
// Get the stop for city information to improve search
const stop = this.data.stops[stopIndex];
let cityParam = '';
let stateParam = '';
if (stop && stop.city_name) {
cityParam = `&city=${encodeURIComponent(stop.city_name)}`;
}
if (stop && stop.state) {
stateParam = `&state=${encodeURIComponent(stop.state)}`;
}
// Clear previous error status
this.showStatus('', '');
// Check if the search term is a location ID (numeric and/or uppercase letters)
const isIdSearch = /^[A-Z0-9]+$/.test(searchTerm.trim());
// Create direct URL to search for the location or ID
let searchUrl;
if (isIdSearch) {
searchUrl = `search-location.php?id=${encodeURIComponent(searchTerm.trim())}`;
} else {
// Create a search with the three main parameters
searchUrl = `search-location.php?name=${encodeURIComponent(searchTerm.trim())}`;
// Only add city and state if they exist
if (stop && stop.city_name) {
searchUrl += `&city_name=${encodeURIComponent(stop.city_name)}`;
}
if (stop && stop.state) {
searchUrl += `&state=${encodeURIComponent(stop.state)}`;
}
}
console.log(`Searching for location with term: "${searchTerm}"`);
console.log(`Using search URL: ${searchUrl}`);
// Show user we're searching
this.showStatus(`Searching for "${searchTerm}"...`, 'info');
fetch(searchUrl)
.then(response => {
console.log(`Search response status: ${response.status}`);
return response.text().then(text => {
console.log('Raw search response:', text);
try {
return { isJson: true, data: JSON.parse(text) };
} catch (e) {
console.error('JSON parse error:', e);
return { isJson: false, data: text };
}
});
})
.then(({ isJson, data }) => {
if (isJson && data.success && data.results && data.results.length > 0) {
const location = data.results[0];
console.log('Found location:', location);
// Update stop data
this.data.stops[stopIndex].location_id = location.id;
this.data.stops[stopIndex].location_name = location.name;
this.data.stops[stopIndex].address = location.address1 || location.address;
this.data.stops[stopIndex].city_name = location.city_name;
this.data.stops[stopIndex].state = location.state;
// Set zip based on stop type (pickup vs delivery)
if (stopIndex === 0 || this.data.stops[stopIndex].stop_type === 'PU') {
this.data.stops[stopIndex].zip_code = location.zip_code;
} else {
this.data.stops[stopIndex].zip = location.zip_code;
}
// If Wilkes Barre, ensure correct city_id
if (location.city_name && location.city_name.toUpperCase().includes('WILKES BARRE')) {
this.data.stops[stopIndex].city_id = "228607";
} else if (location.city_id) {
this.data.stops[stopIndex].city_id = location.city_id;
}
// Reset any styling
if (locationIdInput) {
locationIdInput.classList.remove('bg-yellow-100');
locationIdInput.classList.add('bg-green-100');
locationIdInput.setAttribute('placeholder', 'Location ID');
locationIdInput.value = location.id;
// Reset style after a delay
setTimeout(() => {
locationIdInput.classList.remove('bg-green-100');
}, 2000);
}
// Show success message
this.showStatus(`Location found: ${location.name} (ID: ${location.id})`, 'success');
setTimeout(() => {
this.showStatus('', '');
}, 3000);
// Re-render to show updated fields
this.render();
} else {
// No results found or error in response
console.warn('No location found or API error:', data);
// Reset input field
if (locationIdInput) {
locationIdInput.classList.remove('bg-yellow-100');
locationIdInput.classList.add('bg-red-100');
locationIdInput.value = '';
locationIdInput.setAttribute('placeholder', 'Location not found');
// Reset style after a delay
setTimeout(() => {
locationIdInput.classList.remove('bg-red-100');
locationIdInput.setAttribute('placeholder', 'Location ID');
}, 3000);
}
// Show error message
if (isJson && data.error) {
this.showStatus(`Error: ${data.error}`, 'error');
} else {
this.showStatus(`Location "${searchTerm}" not found in McLeod system. Please try a different search term.`, 'error');
}
}
})
.catch(error => {
console.error('Location search error:', error);
// Reset input field
if (locationIdInput) {
locationIdInput.classList.remove('bg-yellow-100');
locationIdInput.classList.add('bg-red-100');
locationIdInput.value = '';
locationIdInput.setAttribute('placeholder', 'Error');
// Reset style after a delay
setTimeout(() => {
locationIdInput.classList.remove('bg-red-100');
locationIdInput.setAttribute('placeholder', 'Location ID');
}, 3000);
}
// Show error message
this.showStatus(`Search error: ${error.message}. Please try again.`, 'error');
});
}
searchCommodity(commodityTerm) {
if (!commodityTerm || commodityTerm.length < 3) {
this.commoditySuggestions = [];
this.updateCommoditySuggestions();
return;
}
// Show searching indicator
const commodityInput = document.querySelector(`input[data-field="commodity"]`);
const commodityIdInput = document.querySelector(`input[data-field="commodity_id"]`);
if (commodityInput) {
commodityInput.classList.add('bg-yellow-100');
}
fetch(`commodity-lookup.php?action=search&term=${encodeURIComponent(commodityTerm)}`)
.then(response => response.json())
.then(data => {
if (data.success && data.results) {
this.commoditySuggestions = data.results;
// If we have a single exact match, auto-select it
if (data.results.length === 1 &&
data.results[0].description.toLowerCase() === commodityTerm.toLowerCase()) {
this.selectCommodity(data.results[0].code);
} else {
this.updateCommoditySuggestions();
}
} else {
this.commoditySuggestions = [];
this.updateCommoditySuggestions();
}
// Reset styling
if (commodityInput) {
commodityInput.classList.remove('bg-yellow-100');
}
})
.catch(error => {
console.error('Commodity search error:', error);
this.commoditySuggestions = [];
this.updateCommoditySuggestions();
// Reset styling
if (commodityInput) {
commodityInput.classList.remove('bg-yellow-100');
}
});
}
selectCommodity(code) {
fetch(`commodity-lookup.php?action=get&code=${encodeURIComponent(code)}`)
.then(response => response.json())
.then(data => {
if (data.success && data.commodity) {
const commodity = data.commodity;
// Update commodity fields
this.data.commodity_id = code;
this.data.commodity = commodity.description.toUpperCase();
// Update input fields
const commodityInput = document.querySelector(`input[data-field="commodity"]`);
const commodityIdInput = document.querySelector(`input[data-field="commodity_id"]`);
if (commodityInput) {
commodityInput.value = commodity.description.toUpperCase();
commodityInput.classList.add('bg-green-100');
setTimeout(() => {
commodityInput.classList.remove('bg-green-100');
}, 2000);
}
if (commodityIdInput) {
commodityIdInput.value = code;
}
// Clear suggestions
this.commoditySuggestions = [];
this.updateCommoditySuggestions();
}
})
.catch(error => {
console.error('Commodity selection error:', error);
});
}
updateCommoditySuggestions() {
const suggestionsContainer = document.getElementById('commoditySuggestions');
if (!suggestionsContainer) return;
if (this.commoditySuggestions.length === 0) {
suggestionsContainer.innerHTML = '';
suggestionsContainer.classList.add('hidden');
return;
}
let html = `
`;
this.commoditySuggestions.forEach(item => {
html += `
${item.code} - ${item.description}
${item.category}
`;
});
html += `
`;
suggestionsContainer.innerHTML = html;
suggestionsContainer.classList.remove('hidden');
}
renderLocationControls(stopIndex, stop) {
// Ensure stop has all required properties to avoid undefined errors
stop.location_id = stop.location_id || '';
stop.location_name = stop.location_name || '';
stop.address = stop.address || '';
stop.city_name = stop.city_name || '';
stop.state = stop.state || '';
stop.zip_code = stop.zip_code || stop.zip || '';
// Determine if this is a pickup (shipper) location
const isShipper = stop.stop_type === 'PU' || stopIndex === 0;
// Add special highlighting for shipper location which is required
const requiredClass = isShipper ? 'text-red-700 font-bold' : 'text-gray-700';
const requiredText = isShipper ? '* (Required for order creation)' : '';
return `
${!stop.location_id ? `` : ''}
${stop.location_id ? '' : `
Enter location ID, search by name, or create new
`}
${isShipper && !stop.location_id ? `
McLeod requires a valid location ID for the shipper
` : ''}
`;
}
renderScheduleTimeControls(stopIndex, stop) {
// Determine if this is a delivery stop
const isDelivery = stop.stop_type === 'SO' || stopIndex > 0;
// Get pickup time for delivery stop validation
let pickupTimeWarning = '';
if (isDelivery && this.data.stops[0] && this.data.stops[0].sched_arrive_early) {
const pickupTime = new Date(this.formatDateTimeForInput(this.data.stops[0].sched_arrive_early));
const deliveryTime = stop.sched_arrive_early ? new Date(this.formatDateTimeForInput(stop.sched_arrive_early)) : null;
if (deliveryTime && deliveryTime < pickupTime) {
pickupTimeWarning = `Delivery time must be after pickup time (${this.formatReadableTime(pickupTime)})
`;
}
}
return `
${pickupTimeWarning}
`;
}
// FIXED: Updated to use driver_load_unload instead of individual fields
validateDriverLoadUnloadSettings() {
if (!this.data.stops || this.data.stops.length === 0) {
return false;
}
// Check first stop - set driver_load_unload to "LL" for pickup
if (this.data.stops[0]) {
this.data.stops[0].driver_load_unload = 'LL';
// Remove individual driver_load if present
if (this.data.stops[0].driver_load) {
delete this.data.stops[0].driver_load;
}
// Remove driver_unload if present
if (this.data.stops[0].driver_unload) {
delete this.data.stops[0].driver_unload;
}
}
// Check other stops - set driver_load_unload to "LU" for delivery
for (let i = 1; i < this.data.stops.length; i++) {
this.data.stops[i].driver_load_unload = 'LU';
// Remove individual driver_load if present
if (this.data.stops[i].driver_load) {
delete this.data.stops[i].driver_load;
}
// Remove driver_unload if present
if (this.data.stops[i].driver_unload) {
delete this.data.stops[i].driver_unload;
}
}
return true;
}
manualCreateLocation(stopIndex) {
const stop = this.data.stops[stopIndex];
// Validate required fields
if (!stop.location_name || !stop.city_name || !stop.state) {
this.showStatus('Please fill in location name, city, and state before creating a location', 'error');
return;
}
// Prepare location data in McLeod format
const locationData = {
"__type": "location",
"company_id": "TMS",
"name": stop.location_name || '',
"address1": stop.address || '',
"city_name": stop.city_name || '',
"state": stop.state || '',
"zip_code": stop.zip_code || stop.zip || '',
"is_active": 'Y',
"category": stop.stop_type === 'PU' ? 'SHIPPER' : 'CONSIGNEE',
"phone": "0000000000",
"country": "USA"
};
// Show creating message
const locationIdInput = document.querySelector(`input[data-stop="${stopIndex}"][data-field="location_id"]`);
if (locationIdInput) {
locationIdInput.classList.add('bg-blue-100');
locationIdInput.setAttribute('placeholder', 'Creating...');
}
this.showStatus('Creating location in McLeod...', 'info');
fetch('create-location.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(locationData)
})
.then(response => response.json())
.then(data => {
if (data.success && data.location && data.location.id) {
// Update the location ID in the data
this.data.stops[stopIndex].location_id = data.location.id;
// Show success status
this.showStatus(`Location created with ID: ${data.location.id}`, 'success');
// Update the input field
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-green-100');
locationIdInput.value = data.location.id;
setTimeout(() => {
locationIdInput.classList.remove('bg-green-100');
}, 2000);
}
// Re-render to update all fields
this.render();
} else {
// Show error instead
this.showStatus(`Failed to create location: ${data.error || 'Unknown error'}`, 'error');
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-red-100');
locationIdInput.value = '';
locationIdInput.setAttribute('placeholder', 'Creation failed');
setTimeout(() => {
locationIdInput.classList.remove('bg-red-100');
}, 3000);
}
}
})
.catch(error => {
console.error('Error creating location:', error);
// Show error instead
this.showStatus(`Network error creating location: ${error.message}`, 'error');
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-red-100');
locationIdInput.value = '';
locationIdInput.setAttribute('placeholder', 'Network error');
setTimeout(() => {
locationIdInput.classList.remove('bg-red-100');
}, 3000);
}
});
}
createNewLocation(stopIndex, stopData) {
// Prepare location data in exact McLeod format with ALL required fields
const locationData = {
"__type": "location",
"company_id": "TMS",
"name": stopData.location_name || '',
"address1": stopData.address || '',
"city_name": stopData.city_name || '',
"state": stopData.state || '',
"zip_code": stopData.zip_code || stopData.zip || '',
"is_active": 'Y',
"category": stopData.stop_type === 'PU' ? 'SHIPPER' : 'CONSIGNEE',
"phone": "0000000000", // Required field
"country": "USA" // Required field
};
// Show creating status
const locationIdInput = document.querySelector(`input[data-stop="${stopIndex}"][data-field="location_id"]`);
if (locationIdInput) {
locationIdInput.classList.remove('bg-yellow-100', 'bg-orange-100');
locationIdInput.classList.add('bg-blue-100');
locationIdInput.setAttribute('placeholder', 'Creating...');
}
// Log detailed information
console.log('Creating location with data:', locationData);
fetch('create-location.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(locationData)
})
.then(response => response.json())
.then(data => {
console.log('Parsed create location response:', data);
if (data.success && data.location && data.location.id) {
// Update stop with new location data
this.data.stops[stopIndex].location_id = data.location.id;
this.data.stops[stopIndex].location_name = data.location.name || stopData.location_name;
this.data.stops[stopIndex].address = data.location.address1 || stopData.address;
this.data.stops[stopIndex].city_name = data.location.city_name || stopData.city_name;
this.data.stops[stopIndex].state = data.location.state || stopData.state;
// Set zip based on stop type
if (stopIndex === 0 || this.data.stops[stopIndex].stop_type === 'PU') {
this.data.stops[stopIndex].zip_code = data.location.zip_code || stopData.zip_code || stopData.zip;
} else {
this.data.stops[stopIndex].zip = data.location.zip_code || stopData.zip_code || stopData.zip;
}
// If Wilkes Barre, ensure correct city_id
if (data.location.city_name && data.location.city_name.toUpperCase().includes('WILKES BARRE')) {
this.data.stops[stopIndex].city_id = "228607";
}
// Show success message
this.showStatus(`Location created with ID: ${data.location.id}`, 'success');
// Update styling to show success
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-green-100');
locationIdInput.value = data.location.id;
setTimeout(() => {
locationIdInput.classList.remove('bg-green-100');
this.showStatus('', '');
}, 3000);
}
// Re-render to show all updated fields
this.render();
} else {
// If creation failed, use a default location ID
// Show warning but continue with default ID
const defaultLocationId = stopData.city_name && stopData.city_name.toUpperCase().includes('WILKES BARRE') ?
'228607' : 'USCCOTN9';
this.showStatus(`Using default location ID: ${defaultLocationId}`, 'warning');
// Update stop with default location ID
this.data.stops[stopIndex].location_id = defaultLocationId;
// Update styling
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-yellow-100');
locationIdInput.value = defaultLocationId;
setTimeout(() => {
locationIdInput.classList.remove('bg-yellow-100');
this.showStatus('', '');
}, 3000);
}
// Re-render to show updated fields
this.render();
}
})
.catch(error => {
console.error('Error creating location:', error);
// If network error, use a default location ID
const defaultLocationId = stopData.city_name && stopData.city_name.toUpperCase().includes('WILKES BARRE') ?
'228607' : 'USCCOTN9';
this.showStatus(`Using default location ID: ${defaultLocationId}`, 'warning');
// Update stop with default location ID
this.data.stops[stopIndex].location_id = defaultLocationId;
// Update styling
if (locationIdInput) {
locationIdInput.classList.remove('bg-blue-100');
locationIdInput.classList.add('bg-yellow-100');
locationIdInput.value = defaultLocationId;
setTimeout(() => {
locationIdInput.classList.remove('bg-yellow-100');
this.showStatus('', '');
}, 3000);
}
// Re-render to show updated fields
this.render();
});
}
getCustomerIdFromLocationData(locationId) {
if (!locationId) return '';
return new Promise((resolve, reject) => {
fetch(`search-location.php?location.id=${encodeURIComponent(locationId)}`)
.then(response => response.json())
.then(data => {
if (data.success && data.results && data.results.length > 0) {
const location = data.results[0];
resolve(location.customer_id || '');
} else {
resolve('');
}
})
.catch(error => {
console.error('Error fetching customer ID:', error);
resolve('');
});
});
}
validateShipperLocation() {
// Find the shipper stop (first stop or one with stop_type PU)
const shipperStop = this.data.stops.find(stop => stop.stop_type === 'PU') || this.data.stops[0];
if (!shipperStop) {
this.showStatus('Missing pickup stop', 'error');
return false;
}
// Check if the shipper has a valid location_id
if (!shipperStop.location_id) {
this.showStatus('A location code is required for the shipper of the order. Please enter a valid location ID for the pickup stop.', 'error');
// Highlight the location ID field for the shipper stop
const shipperIndex = this.data.stops.indexOf(shipperStop);
const locationIdInput = document.querySelector(`input[data-stop="${shipperIndex}"][data-field="location_id"]`);
if (locationIdInput) {
locationIdInput.classList.add('border-red-500', 'bg-red-50');
locationIdInput.focus();
}
return false;
}
return true;
}
validateStopSequence() {
// Check if we have at least one stop
if (!this.data.stops || this.data.stops.length === 0) {
this.showStatus('At least one stop is required', 'error');
return false;
}
// Make sure the last stop is a delivery stop
const lastStop = this.data.stops[this.data.stops.length - 1];
if (lastStop.stop_type === 'PU') {
this.showStatus('The final stop must be a delivery (SO). McLeod does not accept orders ending with a pickup.', 'error');
// Highlight the last stop
const stopEntries = document.querySelectorAll('.stop-entry');
if (stopEntries.length > 0) {
const lastStopEntry = stopEntries[stopEntries.length - 1];
lastStopEntry.classList.add('border-red-500');
lastStopEntry.scrollIntoView({ behavior: 'smooth' });
}
return false;
}
// Make sure we have at least one pickup
const hasPickup = this.data.stops.some(stop => stop.stop_type === 'PU');
if (!hasPickup) {
this.showStatus('At least one pickup (PU) stop is required', 'error');
return false;
}
return true;
}
// Simple MC number search method
searchCustomerByMCNumber() {
const mcNumberInput = document.querySelector('input[data-field="mc_number"]');
const customerIdInput = document.querySelector('input[data-field="customer_id"]');
const customerNameInput = document.querySelector('input[data-field="broker_name"]');
if (!mcNumberInput || !customerIdInput) return;
let mcNumber = mcNumberInput.value.trim();
if (!mcNumber) return;
// Show searching indicator
customerIdInput.classList.add('bg-yellow-100');
customerIdInput.setAttribute('placeholder', 'Searching...');
// Construct API URL
const searchUrl = `search-customer.php?mc_number=${encodeURIComponent(mcNumber)}`;
console.log(`Searching for customer with MC Number: "${mcNumber}"`);
this.showStatus(`Searching for customer with MC Number: "${mcNumber}"...`, 'info');
fetch(searchUrl)
.then(response => response.json())
.then(data => {
if (data.success && data.customerId) {
// Update the customer ID input and data model
customerIdInput.value = data.customerId;
this.data.customer_id = data.customerId;
// If we have a customer name, update the broker name field
if (data.customerName && customerNameInput) {
customerNameInput.value = data.customerName;
this.data.broker_name = data.customerName;
}
// Visual feedback
customerIdInput.classList.remove('bg-yellow-100');
customerIdInput.classList.add('bg-green-100');
setTimeout(() => {
customerIdInput.classList.remove('bg-green-100');
}, 2000);
this.showStatus(`Found customer: ${data.customerName || data.customerId}`, 'success');
} else {
// If no match found, clear fields and show error
customerIdInput.classList.remove('bg-yellow-100');
customerIdInput.classList.add('bg-red-100');
setTimeout(() => {
customerIdInput.classList.remove('bg-red-100');
}, 2000);
this.showStatus(`No customer found with MC Number: ${mcNumber}`, 'error');
}
})
.catch(error => {
// Handle error
customerIdInput.classList.remove('bg-yellow-100');
this.showStatus(`Search error: ${error.message}`, 'error');
});
}
validateTimeSequence() {
let pickupTime = null;
let errors = [];
// Find pickup time first
for (let i = 0; i < this.data.stops.length; i++) {
const stop = this.data.stops[i];
if (stop.stop_type === 'PU' || i === 0) {
if (stop.sched_arrive_early) {
pickupTime = new Date(this.convertMcleodDateToISOString(stop.sched_arrive_early));
}
break;
}
}
if (!pickupTime) {
return true; // No pickup time to validate against
}
// Check all delivery stops to ensure they're after pickup
for (let i = 0; i < this.data.stops.length; i++) {
const stop = this.data.stops[i];
if (stop.stop_type === 'SO' && stop.sched_arrive_early) {
const deliveryTime = new Date(this.convertMcleodDateToISOString(stop.sched_arrive_early));
if (deliveryTime < pickupTime) {
errors.push(`Delivery stop ${i+1} (${stop.location_name}) is scheduled before pickup. This creates a logical impossibility.`);
// Highlight the time field
const timeInput = document.querySelector(`input[data-stop="${i}"][data-field="sched_arrive_early"]`);
if (timeInput) {
timeInput.classList.add('border-red-500', 'bg-red-50');
}
}
}
}
if (errors.length > 0) {
this.showStatus(errors.join('
'), 'error');
return false;
}
return true;
}
// Helper method to convert McLeod date format to ISO string
convertMcleodDateToISOString(mcleodDate) {
// Format: "20250311130000-0600" to ISO
if (!mcleodDate) return null;
try {
// Extract components from "YYYYMMDDHHmmss-0600"
const year = mcleodDate.substring(0, 4);
const month = mcleodDate.substring(4, 6);
const day = mcleodDate.substring(6, 8);
const hour = mcleodDate.substring(8, 10);
const minute = mcleodDate.substring(10, 12);
// Create an ISO string
return `${year}-${month}-${day}T${hour}:${minute}:00`;
} catch (e) {
console.error('Error parsing McLeod date:', e);
return null;
}
}
validateScheduleTimes(changedStopIndex) {
// First find if we're validating a pickup or delivery stop
const changedStop = this.data.stops[changedStopIndex];
if (!changedStop) return;
const isPickup = changedStop.stop_type === 'PU' || changedStopIndex === 0;
if (isPickup) {
// If pickup time changed, validate all delivery stops against it
const pickupTime = changedStop.sched_arrive_early ?
new Date(this.formatDateTimeForInput(changedStop.sched_arrive_early)) : null;
if (!pickupTime) return;
// Check all delivery stops
for (let i = 0; i < this.data.stops.length; i++) {
if (i === changedStopIndex) continue; // Skip the pickup stop
const stop = this.data.stops[i];
if (stop.stop_type === 'SO' && stop.sched_arrive_early) {
const deliveryTime = new Date(this.formatDateTimeForInput(stop.sched_arrive_early));
if (deliveryTime < pickupTime) {
// Auto-adjust delivery time to be 1 hour after pickup
const newDeliveryTime = new Date(pickupTime);
newDeliveryTime.setHours(newDeliveryTime.getHours() + 1);
// Update the stop data and the input field
this.data.stops[i].sched_arrive_early = this.formatDateTimeForMcleod(newDeliveryTime);
const timeInput = document.querySelector(`input[data-stop="${i}"][data-field="sched_arrive_early"]`);
if (timeInput) {
timeInput.value = this.formatDateTimeForInput(this.data.stops[i].sched_arrive_early);
timeInput.classList.add('bg-yellow-100');
setTimeout(() => {
timeInput.classList.remove('bg-yellow-100');
}, 2000);
}
// Also update the late time if needed
if (stop.sched_arrive_late) {
const lateTime = new Date(this.formatDateTimeForInput(stop.sched_arrive_late));
if (lateTime < newDeliveryTime) {
const newLateTime = new Date(newDeliveryTime);
newLateTime.setHours(newLateTime.getHours() + 1);
this.data.stops[i].sched_arrive_late = this.formatDateTimeForMcleod(newLateTime);
const lateInput = document.querySelector(`input[data-stop="${i}"][data-field="sched_arrive_late"]`);
if (lateInput) {
lateInput.value = this.formatDateTimeForInput(this.data.stops[i].sched_arrive_late);
}
}
}
}
}
}
} else {
// If delivery time changed, validate against pickup time
const pickupStop = this.data.stops.find(stop => stop.stop_type === 'PU') || this.data.stops[0];
if (!pickupStop || !pickupStop.sched_arrive_early) return;
const pickupTime = new Date(this.formatDateTimeForInput(pickupStop.sched_arrive_early));
const deliveryTime = changedStop.sched_arrive_early ?
new Date(this.formatDateTimeForInput(changedStop.sched_arrive_early)) : null;
if (!deliveryTime) return;
if (deliveryTime < pickupTime) {
// Auto-adjust delivery time to be 1 hour after pickup
const newDeliveryTime = new Date(pickupTime);
newDeliveryTime.setHours(newDeliveryTime.getHours() + 1);
// Update the stop data and the input field
this.data.stops[changedStopIndex].sched_arrive_early = this.formatDateTimeForMcleod(newDeliveryTime);
const timeInput = document.querySelector(`input[data-stop="${changedStopIndex}"][data-field="sched_arrive_early"]`);
if (timeInput) {
timeInput.value = this.formatDateTimeForInput(this.data.stops[changedStopIndex].sched_arrive_early);
timeInput.classList.add('bg-yellow-100');
setTimeout(() => {
timeInput.classList.remove('bg-yellow-100');
}, 2000);
}
// Show warning
this.showStatus(`Delivery time must be after pickup time (${this.formatReadableTime(pickupTime)}). Automatically adjusted.`, 'warning');
setTimeout(() => {
this.showStatus('', '');
}, 5000);
}
}
}
// Helper method to format date in a readable way for users
formatReadableTime(dateObj) {
if (!dateObj || !(dateObj instanceof Date)) return '';
const options = {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
};
return dateObj.toLocaleString('en-US', options);
}
render() {
window.mcleodEditor = this;
const container = document.createElement('div');
container.className = 'mcleod-editor mt-6 p-4 border rounded-lg';
container.innerHTML = `
Edit McLeod Order
${this.renderBasicInfo()}
${this.renderStops()}
`;
const editorContainer = document.getElementById('mcleodEditorContainer');
editorContainer.innerHTML = '';
editorContainer.appendChild(container);
this.attachEventListeners();
}
renderBasicInfo() {
return `
`;
}
renderStops() {
return `
Stops
${this.data.stops.map((stop, index) => `
${stop.stop_type === 'PU' ? 'Pickup' : 'Delivery'} Stop ${index + 1}
`).join('')}
`;
}
searchCustomerId() {
const brokerNameInput = document.querySelector('input[data-field="broker_name"]');
const customerIdInput = document.querySelector('input[data-field="customer_id"]');
if (!brokerNameInput || !customerIdInput) return;
let brokerName = brokerNameInput.value.trim();
if (!brokerName) return;
// Normalize broker name - make UPPERCASE to match McLeod API expectations
brokerName = this.normalizeBrokerName(brokerName);
brokerNameInput.value = brokerName; // Update the input with normalized name
// Store the normalized broker name in the data model
this.data.broker_name = brokerName;
// Show searching indicator
customerIdInput.classList.add('bg-yellow-100');
customerIdInput.setAttribute('placeholder', 'Searching...');
// Construct API URL with proper formatting (space as %20, not URL encoded +)
const searchUrl = `search-customer.php?name=${encodeURIComponent(brokerName)}`;
console.log(`Searching for customer ID with broker name: "${brokerName}"`);
this.showStatus(`Searching for customer: "${brokerName}"...`, 'info');
fetch(searchUrl)
.then(response => {
console.log(`Search response status: ${response.status}`);
return response.text().then(text => {
try {
return { isJson: true, data: JSON.parse(text) };
} catch (e) {
console.error('JSON parse error:', e);
return { isJson: false, data: text };
}
});
})
.then(({ isJson, data }) => {
if (isJson && data.success && data.customerId) {
// Update the customer ID input and data model
customerIdInput.value = data.customerId;
this.data.customer_id = data.customerId;
// Visual feedback
customerIdInput.classList.remove('bg-yellow-100');
customerIdInput.classList.add('bg-green-100');
setTimeout(() => {
customerIdInput.classList.remove('bg-green-100');
}, 2000);
this.showStatus(`Found customer ID: ${data.customerId} for broker: ${brokerName}`, 'success');
} else {
// If no match found, use default
const defaultId = '10ROHOIL';
customerIdInput.value = defaultId;
this.data.customer_id = defaultId;
// Visual feedback
customerIdInput.classList.remove('bg-yellow-100');
customerIdInput.classList.add('bg-orange-100');
setTimeout(() => {
customerIdInput.classList.remove('bg-orange-100');
}, 2000);
this.showStatus(`Using default customer ID: ${defaultId} (no match for: ${brokerName})`, 'warning');
}
})
.catch(error => {
console.error('Customer search error:', error);
// If error, use default
const defaultId = '10ROHOIL';
customerIdInput.value = defaultId;
this.data.customer_id = defaultId;
// Visual feedback
customerIdInput.classList.remove('bg-yellow-100');
customerIdInput.classList.add('bg-red-100');
setTimeout(() => {
customerIdInput.classList.remove('bg-red-100');
}, 2000);
this.showStatus(`Customer search error: ${error.message}. Using default ID.`, 'error');
});
}
formatDateTimeForInput(mcleodDateTime) {
// Add null check
if (!mcleodDateTime) return '';
try {
// Handle both formats: "YYYYMMDDHHMMSS-0600" and "YYYY-MM-DD HHMM"
let match;
if (mcleodDateTime.includes('-0600')) {
match = mcleodDateTime.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/);
} else {
match = mcleodDateTime.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2})(\d{2})/);
}
if (match) {
return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`;
}
return '';
} catch (e) {
console.error('Date parsing error:', e);
return '';
}
}
formatDateTimeForMcleod(inputDateTime) {
// Convert from "YYYY-MM-DDTHH:MM" to "YYYYMMDDHHMMSS-0600"
if (!inputDateTime) return '';
// Handle if inputDateTime is a Date object
if (inputDateTime instanceof Date) {
const year = inputDateTime.getFullYear();
const month = String(inputDateTime.getMonth() + 1).padStart(2, '0');
const day = String(inputDateTime.getDate()).padStart(2, '0');
const hours = String(inputDateTime.getHours()).padStart(2, '0');
const minutes = String(inputDateTime.getMinutes()).padStart(2, '0');
const seconds = String(inputDateTime.getSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}-0600`;
}
// Handle if inputDateTime is a string
return inputDateTime.replace(/[-:T]/g, '') + '00-0600';
}
attachEventListeners() {
// Download JSON button handler
document.getElementById('downloadMcLeodBtn').addEventListener('click', () => {
if (this.validateAllFields()) {
const jsonData = this.collectFormData();
this.downloadJson(jsonData);
}
});
// Send to McLeod button handler
document.getElementById('sendToMcLeodBtn').addEventListener('click', () => {
if (this.validateAllFields()) {
this.sendToMcLeod();
}
});
document.querySelectorAll('.mcleod-editor input[type="number"]').forEach(input => {
input.addEventListener('change', (e) => {
const field = e.target.dataset.field;
const stopIndex = e.target.dataset.stop;
let value = parseFloat(e.target.value) || 0;
if (stopIndex !== undefined) {
this.data.stops[stopIndex][field] = value;
} else {
this.data[field] = value;
}
});
});
// Add event listener for MC Number field
const mcNumberInput = document.querySelector('input[data-field="mc_number"]');
if (mcNumberInput) {
// Use both blur and keypress (Enter) events
mcNumberInput.addEventListener('blur', () => {
if (mcNumberInput.value.trim()) {
this.searchCustomerByMCNumber();
}
});
mcNumberInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && mcNumberInput.value.trim()) {
e.preventDefault(); // Prevent form submission
this.searchCustomerByMCNumber();
}
});
}
// Keep the broker name event listener for backward compatibility
const brokerNameInput = document.querySelector('input[data-field="broker_name"]');
if (brokerNameInput) {
// Use both blur and change events to increase chance of triggering
brokerNameInput.addEventListener('blur', () => {
this.searchCustomerId();
});
brokerNameInput.addEventListener('change', () => {
this.searchCustomerId();
});
// If we have broker name but no search has been done, trigger search
if (brokerNameInput.value && !document.querySelector('input[data-field="customer_id"]').value) {
this.searchCustomerId();
}
}
// Handle clicks outside the commodity suggestions to close them
document.addEventListener('click', (e) => {
const suggestionsContainer = document.getElementById('commoditySuggestions');
const commodityInput = document.querySelector('input[data-field="commodity"]');
if (suggestionsContainer &&
!suggestionsContainer.contains(e.target) &&
(!commodityInput || e.target !== commodityInput)) {
suggestionsContainer.innerHTML = '';
suggestionsContainer.classList.add('hidden');
}
});
// Handle initial commodity lookup if we have a description but no code
if (this.data.commodity && (!this.data.commodity_id || this.data.commodity_id === '')) {
this.searchCommodity(this.data.commodity);
}
}
validateAllFields() {
const requiredInputs = document.querySelectorAll('.mcleod-editor input[required]');
let isValid = true;
requiredInputs.forEach(input => {
if (!this.validateField(input)) {
isValid = false;
}
});
if (!isValid) {
alert('Please fill in all required fields (marked with *)');
}
return isValid;
}
validateField(input) {
if (input.hasAttribute('required')) {
const value = input.value.trim();
if (!value) {
input.classList.add('border-red-500');
return false;
}
}
input.classList.remove('border-red-500');
return true;
}
collectFormData() {
// First update the data object with values from all inputs
document.querySelectorAll('.mcleod-editor input, .mcleod-editor select').forEach(input => {
const field = input.dataset.field;
const stopIndex = input.dataset.stop;
let value = input.value;
// Special handling for datetime fields
if (input.type === 'datetime-local') {
value = this.formatDateTimeForMcleod(value);
}
// Special handling for number fields
if (input.type === 'number') {
value = parseFloat(value) || 0;
}
if (stopIndex !== undefined) {
// CRITICAL: Ensure we're creating the stops array if it doesn't exist
if (!this.data.stops) {
this.data.stops = [];
}
// CRITICAL: Ensure stop object exists at this index
if (!this.data.stops[stopIndex]) {
this.data.stops[stopIndex] = {};
}
// CRITICAL: Extra logging for location_id field
if (field === 'location_id') {
console.log(`Setting location_id for stop ${stopIndex} to: ${value}`);
}
this.data.stops[stopIndex][field] = value;
} else {
this.data[field] = value;
}
});
// CRITICAL: Fix driver_load_unload settings in all stops
if (this.data.stops && this.data.stops.length > 0) {
console.log("LOCATION IDs AFTER COLLECTION:",
this.data.stops.map(stop => stop.location_id || 'MISSING'));
// Make sure the first stop is ALWAYS a pickup with "LL" driver_load_unload
this.data.stops[0].stop_type = 'PU';
this.data.stops[0].driver_load_unload = 'LL'; // THIS is the key change
// Remove individual driver_load/driver_unload fields if present
if (this.data.stops[0].driver_load) {
delete this.data.stops[0].driver_load;
}
if (this.data.stops[0].driver_unload) {
delete this.data.stops[0].driver_unload;
}
// Make sure all other stops are ALWAYS delivery with "LU" driver_load_unload
for (let i = 1; i < this.data.stops.length; i++) {
this.data.stops[i].stop_type = 'SO';
this.data.stops[i].driver_load_unload = 'LU'; // THIS is the key change
// Remove individual driver_load/driver_unload fields if present
if (this.data.stops[i].driver_load) {
delete this.data.stops[i].driver_load;
}
if (this.data.stops[i].driver_unload) {
delete this.data.stops[i].driver_unload;
}
}
}
return this.data;
}
// Helper method to format current date for McLeod
formatDateForMcleod() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}-0600`;
}
sendToMcLeod() {
// Show loading state
const sendButton = document.getElementById('sendToMcLeodBtn');
const originalButtonText = sendButton.textContent;
sendButton.textContent = 'Sending...';
sendButton.disabled = true;
// Clear any previous status messages
this.showStatus('', '');
// Reset all field highlighting
document.querySelectorAll('.mcleod-editor input').forEach(input => {
input.classList.remove('border-red-500', 'bg-red-50');
});
document.querySelectorAll('.stop-entry').forEach(stopEntry => {
stopEntry.classList.remove('border-red-500');
});
// Run validations
if (!this.validateShipperLocation()) {
sendButton.textContent = originalButtonText;
sendButton.disabled = false;
return;
}
if (!this.validateTimeSequence()) {
sendButton.textContent = originalButtonText;
sendButton.disabled = false;
return;
}
// Add the new validation for stop sequence
if (!this.validateStopSequence()) {
sendButton.textContent = originalButtonText;
sendButton.disabled = false;
return;
}
// CRITICAL: Ensure driver_load_unload settings are correct
this.validateDriverLoadUnloadSettings();
// Apply aggressive fix for stop types before getting form data
this.fixStopTypes();
// Get updated form data
const originalData = this.collectFormData();
// Set equipment type based codes
const equipmentTypeId = originalData.equipment_type_id || "R";
let revenueCodeId = "REFC"; // Default for Reefer
// Map equipment type to revenue code
if (equipmentTypeId === "V") {
revenueCodeId = "DRYCD"; // Dry Van
} else if (equipmentTypeId === "F") {
revenueCodeId = "FLTLP"; // Flatbed
}
// Always use 10ROHOIL as the customer_id
const customerId = originalData.customer_id || "10ROHOIL";
// Create properly ordered McLeod data structure with correct format
const formattedOrder = {
"__type": "orders",
"company_id": "TMS",
"status": "A",
"blnum": originalData.blnum || "",
"commodity": originalData.commodity || "",
"collection_method": "P",
"customer_id": customerId,
"equipment_type_id": equipmentTypeId,
"ordered_date": originalData.ordered_date || this.formatDateForMcleod(),
"revenue_code_id": revenueCodeId,
"temperature_min": parseFloat(originalData.temperature_min || -15),
"temperature_max": parseFloat(originalData.temperature_max || -15),
"setpoint_temp": parseFloat(originalData.setpoint_temp || -10),
"freight_charge": parseFloat(originalData.freight_charge || 1000),
"total_charge": parseFloat(originalData.freight_charge || 1000),
"order_mode": "B",
"loadboard": "D",
"rate_type": "F",
"rate_units": 1.0000,
"bill_distance_um": "MI",
"stops": []
};
// Add commodity_id if it exists
if (originalData.commodity_id) {
formattedOrder.commodity_id = originalData.commodity_id;
}
// Copy stops with all dynamic values, ensuring correct driver_load_unload settings
formattedOrder.stops = (originalData.stops || []).map((stop, index) => {
// Force stop type setting based on index
const stopType = (index === 0) ? "PU" : "SO";
const newStop = {
"__type": "stop",
"__name": "stops",
"stop_type": stopType,
"sequence": index + 1,
"location_id": stop.location_id || "",
"location_name": stop.location_name || "",
"address": stop.address || "",
"city_name": stop.city_name || "",
"state": stop.state || "",
"driver_load_unload": index === 0 ? "LL" : "LU", // CRITICAL: Use consolidated field
"zip_code": stop.zip_code || stop.zip || ""
};
// Correct the city_id for Wilkes Barre
if (stop.city_name && stop.city_name.toUpperCase().includes('WILKES BARRE')) {
newStop.city_id = "228607"; // Correct city ID for Wilkes Barre, PA
} else if (stop.city_id) {
newStop.city_id = stop.city_id;
}
// Add schedule times
if (stop.sched_arrive_early) {
newStop.sched_arrive_early = stop.sched_arrive_early;
}
if (stop.sched_arrive_late) {
newStop.sched_arrive_late = stop.sched_arrive_late;
}
return newStop;
});
// Double check final stop is delivery type
if (formattedOrder.stops.length > 0) {
const lastIndex = formattedOrder.stops.length - 1;
formattedOrder.stops[lastIndex].stop_type = "SO";
console.log('Forced last stop to SO in formattedOrder');
console.log('Final stop types:', formattedOrder.stops.map(stop => stop.stop_type));
}
// Add freight group
formattedOrder.freightGroup = {
"__type": "freight_group",
"__name": "freightGroup",
"description": originalData.commodity || "",
"freightGroupItems": [
{
"__type": "freight_group_item",
"__name": "freightGroupItems",
"pieces": 1,
"weight": 0,
"description": originalData.commodity || ""
}
]
};
// Add other charges ONLY if they exist in the original data
if (originalData.otherCharges && originalData.otherCharges.length > 0) {
formattedOrder.otherCharges = originalData.otherCharges;
} else {
// Default charge if none exists
formattedOrder.otherCharges = [
{
"__type": "other_charge",
"__name": "otherCharges",
"charge_id": "FINE",
"calc_method": "F",
"amount": 50,
"currency_type": "USD",
"description": "Failure to do any of the following will result in a $50.00 fine"
}
];
}
console.log('Sending to McLeod:', formattedOrder);
// Try using our PHP proxy first
fetch('send-to-mcleod.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formattedOrder)
})
.then(response => {
console.log('Response status from proxy:', response.status);
return response.text().then(text => {
try {
// Try to parse as JSON
return { isJson: true, data: JSON.parse(text) };
} catch (e) {
// If not JSON, return as text
return { isJson: false, data: text };
}
});
})
.then(({ isJson, data }) => {
// Reset button state
sendButton.textContent = originalButtonText;
sendButton.disabled = false;
if (isJson && data.success) {
console.log('API success response:', data);
// Show success message
const orderId = data.order && data.order.id ? data.order.id :
(data.id || data.order_id || 'Unknown');
this.showStatus('Order successfully sent to McLeod! Order ID: ' + orderId, 'success');
// Try to upload PDF
if (orderId !== 'Unknown') {
this.uploadPdfToOrder(orderId);
}
} else {
// Show error message - could be JSON error or text
const errorMsg = isJson ?
(data.error || 'Unknown error') :
'Server response: ' + data;
console.error('API error:', errorMsg);
this.showStatus('Error: ' + errorMsg, 'error');
// Try direct API as fallback if PHP proxy fails
this.sendToMcLeodDirect(formattedOrder);
}
})
.catch(error => {
console.error('Fetch error:', error);
// Reset button state
sendButton.textContent = originalButtonText;
sendButton.disabled = false;
// Show error message
this.showStatus('Error connecting to server: ' + error.message, 'error');
// Try direct API as fallback
this.sendToMcLeodDirect(formattedOrder);
});
}
downloadJson(jsonData) {
// First ensure we have the latest form data
const updatedData = this.collectFormData();
// Apply aggressive fix for stop types
this.fixStopTypes();
// Set equipment type based codes
const equipmentTypeId = updatedData.equipment_type_id || "R";
let revenueCodeId = "REFC"; // Default for Reefer
// Map equipment type to revenue code
if (equipmentTypeId === "V") {
revenueCodeId = "DRYCD"; // Dry Van
} else if (equipmentTypeId === "F") {
revenueCodeId = "FLTLP"; // Flatbed
}
// Create a properly formatted JSON object
const formattedJson = {
"__type": "orders",
"company_id": "TMS",
"status": "A",
"blnum": updatedData.blnum || "",
"commodity": updatedData.commodity || "",
"collection_method": "P",
"customer_id": updatedData.customer_id || "10ROHOIL",
"equipment_type_id": equipmentTypeId,
"ordered_date": updatedData.ordered_date || this.formatDateForMcleod(),
"revenue_code_id": revenueCodeId,
"temperature_min": parseFloat(updatedData.temperature_min || -15),
"temperature_max": parseFloat(updatedData.temperature_max || -15),
"setpoint_temp": parseFloat(updatedData.setpoint_temp || -10),
"freight_charge": parseFloat(updatedData.freight_charge || 0),
"total_charge": parseFloat(updatedData.freight_charge || 0),
"stops": updatedData.stops.map((stop, index) => {
// Force stop type setting based on index
const stopType = (index === 0) ? "PU" : "SO";
const stopData = {
"__type": "stop",
"__name": "stops",
"stop_type": stopType, // Override any stored value
"sequence": index + 1,
"location_id": stop.location_id || "",
"location_name": stop.location_name || "",
"address": stop.address || "",
"city_name": stop.city_name || "",
"state": stop.state || "",
"zip_code": stop.zip_code || stop.zip || "",
"driver_load_unload": index === 0 ? "LL" : "LU", // Using consolidated field
"sched_arrive_early": stop.sched_arrive_early || this.formatDateTimeForMcleod(new Date()),
"sched_arrive_late": stop.sched_arrive_late || this.formatDateTimeForMcleod(new Date())
};
// Correct city ID for Wilkes Barre
if (stop.city_name && stop.city_name.toUpperCase().includes('WILKES BARRE')) {
stopData.city_id = "228607";
} else {
stopData.city_id = stop.city_id || (index === 0 ? "259451" : (index === 1 ? "93293" : "93017"));
}
return stopData;
}),
"freightGroup": {
"__type": "freight_group",
"__name": "freightGroup",
"description": updatedData.commodity || "",
"freightGroupItems": [
{
"__type": "freight_group_item",
"__name": "freightGroupItems",
"pieces": 1,
"weight": 0,
"description": updatedData.commodity || ""
}
]
},
"otherCharges": [
{
"__type": "other_charge",
"__name": "otherCharges",
"charge_id": "FINE",
"calc_method": "F",
"amount": 50,
"currency_type": "USD",
"description": "Failure to do any of the following will result in a $50.00 fine"
}
]
};
// Add commodity_id if it exists
if (updatedData.commodity_id) {
formattedJson.commodity_id = updatedData.commodity_id;
}
// Make sure the last stop is a delivery
if (formattedJson.stops.length > 0) {
formattedJson.stops[formattedJson.stops.length - 1].stop_type = "SO";
console.log('Force-set last stop to SO in download');
}
const blob = new Blob([JSON.stringify(formattedJson, null, 4)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mcleod_order_updated.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
sendToMcLeodDirect(formattedOrder) {
this.showStatus('Attempting secondary submission via proxy...', 'info');
// CRITICAL: Force stop types to be correct before sending
// First stop must be PU, all others including the last must be SO
if (formattedOrder.stops && formattedOrder.stops.length > 0) {
// Force first stop to be pickup
formattedOrder.stops[0].stop_type = 'PU';
formattedOrder.stops[0].driver_load_unload = 'LL';
// Force all other stops to be delivery
for (let i = 1; i < formattedOrder.stops.length; i++) {
formattedOrder.stops[i].stop_type = 'SO';
formattedOrder.stops[i].driver_load_unload = 'LU';
}
// Triple-check the last stop is delivery type
const lastIndex = formattedOrder.stops.length - 1;
if (lastIndex > 0) {
formattedOrder.stops[lastIndex].stop_type = 'SO';
console.log('Triple-enforced last stop as SO in direct API call');
}
}
formattedOrder.rate_type = 'F'; // Flat rate
formattedOrder.rate_units = 1.0000; // Always 1.0000
formattedOrder.rate = parseFloat(formattedOrder.freight_charge); // Copy from freight charge
// Add bill_distance if we can determine it
if (formattedOrder.stops && formattedOrder.stops.length >= 2) {
// Default value - ideally would be calculated from actual distance
formattedOrder.bill_distance = 500.0;
formattedOrder.bill_distance_um = 'MI';
}
// FIXED: Use our PHP proxy instead of trying direct API connection
// This avoids CORS errors completely
fetch('send-to-mcleod.php', {
method: 'POST', // Still POST to our proxy, which uses PUT to McLeod API
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formattedOrder)
})
.then(response => {
console.log('Secondary proxy response status:', response.status);
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('Response parsing error:', e, text);
// If parsing fails, create a structured response object
return {
success: false,
error: 'Failed to parse response: ' + e.message,
raw_response: text
};
}
});
})
.then(data => {
console.log('Secondary submission response:', data);
// Check if the submission was successful
if (data.success) {
// Show success message
const orderId = data.order?.id || data.id || 'ORDER-' + new Date().getTime();
this.showStatus('Order successfully created in McLeod. Order ID: ' + orderId, 'success');
// Try to upload PDF
if (orderId !== 'Unknown') {
this.uploadPdfToOrder(orderId);
}
} else {
// Show error but make it clear this was the secondary attempt
this.showStatus('Secondary submission failed: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Secondary submission error:', error);
this.showStatus('Secondary submission error: ' + error.message, 'error');
});
}
uploadPdfToOrder(orderId) {
// Get the uploaded PDF file
const fileInput = document.getElementById('fileInput');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
this.showStatus('Cannot upload PDF: No file found', 'warning');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('pdf_file', file);
formData.append('order_id', orderId);
this.showStatus('Uploading PDF to Order #' + orderId + '...', 'info');
// Send the PDF to McLeod
fetch('upload-pdf-to-mcleod.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.showStatus('PDF successfully attached to Order #' + orderId, 'success');
// Prompt to process another after successful upload
setTimeout(() => {
if (confirm('Order #' + orderId + ' created and PDF attached. Process another rate confirmation?')) {
window.location.reload();
}
}, 3000);
} else {
this.showStatus('PDF upload failed: ' + data.error + ' (Order still created)', 'warning');
}
})
.catch(error => {
this.showStatus('PDF upload error: ' + error.message + ' (Order still created)', 'warning');
});
}
showStatus(message, type) {
const statusArea = document.getElementById('mcleodStatusArea');
const statusMessage = document.getElementById('mcleodStatusMessage');
if (!message) {
statusArea.classList.add('hidden');
return;
}
statusArea.classList.remove('hidden');
statusMessage.innerHTML = message;
// Reset classes
statusMessage.className = 'p-4 rounded';
// Add appropriate styling
if (type === 'success') {
statusMessage.classList.add('bg-green-100', 'text-green-800', 'border', 'border-green-200');
} else if (type === 'error') {
statusMessage.classList.add('bg-red-100', 'text-red-800', 'border', 'border-red-200');
} else if (type === 'warning') {
statusMessage.classList.add('bg-yellow-100', 'text-yellow-800', 'border', 'border-yellow-200');
} else {
statusMessage.classList.add('bg-blue-100', 'text-blue-800', 'border', 'border-blue-200');
}
}
}