Fields are working now as expected. need to remove job size gues and guess source and move schedule confidence to beside start date, but this gets us back on the right path.

This commit is contained in:
2026-01-06 14:29:47 +00:00
parent 701b27cf3f
commit 2546eb30fe
2 changed files with 450 additions and 7 deletions
+402 -7
View File
@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Creation Form</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
@@ -64,6 +66,7 @@
</label>
<input type="text" id="Job_Name" name="Job_Name" required placeholder="Enter job name" autocomplete="off"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<p class="text-xs text-gray-500 mt-1">Use a descriptive name (no symbols-only, no N/A).</p>
</div>
</div>
@@ -162,8 +165,9 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="Phone_Number" class="block text-sm font-medium text-gray-700 mb-2">Phone Number</label>
<input type="tel" id="Phone_Number" name="Phone_Number" placeholder="Enter phone number" autocomplete="tel"
<input type="tel" id="Phone_Number" name="Phone_Number" placeholder="(###) ###-####" autocomplete="tel"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<p class="text-xs text-gray-500 mt-1">Use format (###) ###-####. Extension can go in EXT.</p>
</div>
<div class="flex flex-col">
@@ -173,11 +177,51 @@
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="EXT" class="block text-sm font-medium text-gray-700 mb-2">EXT (optional)</label>
<input type="text" id="EXT" name="EXT" placeholder="Extension" autocomplete="off"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
</div>
<div class="flex flex-col">
<label for="Fax" class="block text-sm font-medium text-gray-700 mb-2">Fax (optional)</label>
<input type="text" id="Fax" name="Fax" placeholder="Fax number" autocomplete="off"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
</div>
</div>
<div class="grid grid-cols-1 gap-6 mb-6">
<div class="flex flex-col">
<label for="Contact_Notes" class="block text-sm font-medium text-gray-700 mb-2">Contact Notes (optional)</label>
<textarea id="Contact_Notes" name="Contact_Notes" rows="2" placeholder="Add extra contact details or instructions"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"></textarea>
</div>
</div>
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-col">
<label for="Job_Address" class="block text-sm font-medium text-gray-700 mb-2">Job Address</label>
<input type="text" id="Job_Address" name="Job_Address" placeholder="Enter job address" autocomplete="street-address"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<div class="flex gap-2">
<input type="text" id="Job_Address" name="Job_Address" placeholder="e.g., 123 Main St, Los Angeles, CA 90012 or 34.0522, -118.2437" autocomplete="street-address"
class="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<button type="button" id="openMapPicker" class="px-4 py-2.5 bg-primary text-white rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2 whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Pick on Map
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Examples: <strong>123 Main St, Los Angeles, CA</strong> or coordinates: <strong>34.0522, -118.2437</strong>. Click "Pick on Map" to select location visually.</p>
</div>
</div>
<div class="grid grid-cols-1 gap-6 mb-6">
<div class="flex flex-col">
<label for="Address_Notes" class="block text-sm font-medium text-gray-700 mb-2">Address Notes (required if address not precise)</label>
<textarea id="Address_Notes" name="Address_Notes" rows="2" placeholder="Add directions or location notes if the address is incomplete"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"></textarea>
</div>
</div>
</div>
@@ -234,8 +278,9 @@
<!-- Start Date (moved to top) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="Start_Date" class="block text-sm font-medium text-gray-700 mb-2">Start Date</label>
<label for="Start_Date" class="block text-sm font-medium text-gray-700 mb-2" title="Estimated date work will start.">Start Date</label>
<input type="text" id="Start_Date" name="Start_Date" class="datepicker w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" placeholder="Select start date" autocomplete="off">
<p class="text-xs text-gray-500 mt-1">Estimated date work will start.</p>
</div>
</div>
@@ -263,19 +308,48 @@
<!-- Due Date and Time (now third) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="Due_Date" class="block text-sm font-medium text-gray-700 mb-2">Due Date</label>
<label for="Due_Date" class="block text-sm font-medium text-gray-700 mb-2" title="Date estimate is due to client.">Due Date</label>
<input type="text" id="Due_Date" name="Due_Date" class="datepicker w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" placeholder="Select due date" autocomplete="off">
<p class="text-xs text-gray-500 mt-1">Date estimate is due to client.</p>
</div>
<div class="flex flex-col">
<label for="Due_Time" class="block text-sm font-medium text-gray-700 mb-2">Due Time</label>
<input type="text" id="Due_Time" name="Due_Time" placeholder="Enter time" autocomplete="off"
<input type="text" id="Due_Time" name="Due_Time" list="due-time-options" placeholder="Select or type" autocomplete="off"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<datalist id="due-time-options">
<option value="5:00 AM"></option>
<option value="5:30 AM"></option>
<option value="6:00 AM"></option>
<option value="6:30 AM"></option>
<option value="7:00 AM"></option>
<option value="7:30 AM"></option>
<option value="8:00 AM"></option>
<option value="8:30 AM"></option>
<option value="9:00 AM"></option>
<option value="9:30 AM"></option>
<option value="10:00 AM"></option>
<option value="10:30 AM"></option>
<option value="11:00 AM"></option>
<option value="11:30 AM"></option>
<option value="12:00 PM"></option>
<option value="12:30 PM"></option>
<option value="1:00 PM"></option>
<option value="1:30 PM"></option>
<option value="2:00 PM"></option>
<option value="2:30 PM"></option>
<option value="3:00 PM"></option>
<option value="3:30 PM"></option>
<option value="4:00 PM"></option>
<option value="4:30 PM"></option>
<option value="5:00 PM"></option>
</datalist>
<p class="text-xs text-gray-500 mt-1">Choose a time or type a custom value.</p>
</div>
</div>
<!-- Due Date Source (now last) -->
<div class="grid grid-cols-1 gap-6">
<div class="grid grid-cols-1 gap-6 mb-6">
<div class="flex flex-col">
<label class="block text-sm font-medium text-gray-700 mb-2">Due Date Source</label>
<div class="flex flex-wrap gap-4">
@@ -294,6 +368,15 @@
</div>
</div>
</div>
<!-- Any other information -->
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-col">
<label for="Notes" class="block text-sm font-medium text-gray-700 mb-2">Any other information we need to know? (optional)</label>
<textarea id="Notes" name="Notes" rows="3" placeholder="Add any additional details, special instructions, or notes"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"></textarea>
</div>
</div>
</div>
@@ -320,6 +403,20 @@
<p id="taxReminder" class="hidden mt-3 text-sm font-semibold text-red-700">Don't forget to file the paperwork!</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mt-6">
<div class="flex flex-col">
<label for="Job_Size_Guess" class="block text-sm font-medium text-gray-700 mb-2" title="Basic financial cost/value of the job. Example: $25,000.00 (just a guess).">Job Size Guess</label>
<input type="text" id="Job_Size_Guess" name="Job_Size_Guess" placeholder="$0,000.00 (guess)"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<p class="text-xs text-gray-500 mt-1">Enter a rough value (e.g., $25,000.00). Will auto-format in USD.</p>
</div>
<div class="flex flex-col">
<label for="Job_Size_Guess_Source" class="block text-sm font-medium text-gray-700 mb-2" title="Who provided this guess? Estimator, Rick, Project Manager, etc.">Guess Source</label>
<input type="text" id="Job_Size_Guess_Source" name="Job_Size_Guess_Source" placeholder="Estimator, Rick, PM, etc."
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
</div>
</div>
</div>
<button type="submit" id="submitBtn" disabled
@@ -331,6 +428,37 @@
</form>
</div>
<!-- Map Picker Modal -->
<div id="mapPickerModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-xl font-bold text-gray-800">Select Location on Map</h2>
<button type="button" id="closeMapPicker" class="text-gray-500 hover:text-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 relative">
<div id="map" class="w-full h-full" style="min-height: 500px;"></div>
</div>
<div class="p-4 border-t border-gray-200 flex items-center justify-between gap-4">
<div class="flex-1">
<p class="text-sm text-gray-600">Click anywhere on the map to drop a pin. Selected coordinates will be added to Job Address.</p>
<p id="selectedCoords" class="text-sm font-medium text-primary mt-1"></p>
</div>
<div class="flex gap-2">
<button type="button" id="cancelMapPicker" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">
Cancel
</button>
<button type="button" id="confirmMapPicker" class="px-4 py-2 bg-primary text-white rounded-lg hover:opacity-90 transition-opacity" disabled>
Use Coordinates
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script>
@@ -459,7 +587,118 @@
// Add event listeners to required fields
document.getElementById('Job_Name').addEventListener('input', checkRequiredFields);
document.getElementById('Office_Rep').addEventListener('change', checkRequiredFields);
// Job name field: validate on blur for 4-char minimum
document.getElementById('Job_Name').addEventListener('blur', (e) => {
const jobName = (e.target.value || '').toString().trim();
const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
const hasLetters = /[A-Za-z]/.test(jobName);
const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
const isLongEnough = jobName.length >= 4;
if (jobName.length > 0 && (!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral)) {
e.target.classList.add('border-red-500', 'bg-red-50');
e.target.title = 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).';
} else {
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.title = '';
}
});
// Phone number field: validate and format on blur
document.getElementById('Phone_Number').addEventListener('blur', (e) => {
const phoneRaw = (e.target.value || '').toString().trim();
if (!phoneRaw) return; // Allow empty
const isUnknown = phoneRaw.toLowerCase() === 'unknown';
if (isUnknown) {
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.title = '';
e.target.value = 'Unknown';
return;
}
const digits = phoneRaw.replace(/\D/g, '').slice(0, 10);
if (digits.length === 10) {
const formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
e.target.value = formatted;
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.title = `Phone formatted as: ${formatted}`;
} else {
e.target.classList.add('border-red-500', 'bg-red-50');
e.target.title = `Phone must be 10 digits (found ${digits.length}) or enter "Unknown".`;
}
});
// Fax field: validate and format on blur (same as phone)
document.getElementById('Fax').addEventListener('blur', (e) => {
const faxRaw = (e.target.value || '').toString().trim();
if (!faxRaw) return; // Allow empty (optional field)
const isUnknown = faxRaw.toLowerCase() === 'unknown';
if (isUnknown) {
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.title = '';
e.target.value = 'Unknown';
return;
}
const digits = faxRaw.replace(/\D/g, '').slice(0, 10);
if (digits.length === 10) {
const formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
e.target.value = formatted;
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.title = `Fax formatted as: ${formatted}`;
} else {
e.target.classList.add('border-red-500', 'bg-red-50');
e.target.title = `Fax must be 10 digits (found ${digits.length}) or enter "Unknown".`;
}
});
// Job Address field: validate address or coordinates on blur
document.getElementById('Job_Address').addEventListener('blur', (e) => {
const addr = (e.target.value || '').toString().trim();
if (!addr) {
e.target.classList.remove('border-red-500', 'bg-red-50', 'border-green-500', 'bg-green-50');
e.target.title = '';
return; // Empty is allowed, will check Address_Notes on submit
}
// Check if it looks like coordinates: pattern "lat, lon" or "lat,lon"
const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
const isCoordinates = coordPattern.test(addr);
// Check if it looks like a real address: has digit(s) and letter(s), at least 5 chars
const hasDigit = /\d/.test(addr);
const hasAlpha = /[A-Za-z]/.test(addr);
const isAddress = addr.length >= 5 && hasDigit && hasAlpha;
if (isCoordinates || isAddress) {
e.target.classList.remove('border-red-500', 'bg-red-50');
e.target.classList.add('border-green-500', 'bg-green-50');
e.target.title = isCoordinates ? 'Valid coordinates format' : 'Valid address format';
} else {
e.target.classList.remove('border-green-500', 'bg-green-50');
e.target.classList.add('border-red-500', 'bg-red-50');
e.target.title = 'Address must be like "123 Main St, City, State" or coordinates like "34.0522, -118.2437". Add directions in Address Notes if needed.';
}
});
document.getElementById('Office_Rep').addEventListener('change', updateSelectPlaceholders);
// Job Size Guess: format as USD on blur
const jobSizeInput = document.getElementById('Job_Size_Guess');
if (jobSizeInput) {
jobSizeInput.addEventListener('blur', (e) => {
const raw = (e.target.value || '').toString().trim();
if (!raw) return;
// Keep only digits and one decimal point
const cleaned = raw.replace(/[^0-9.]/g, '').replace(/(\..*?)\./g, '$1');
const num = parseFloat(cleaned);
if (isNaN(num)) return;
e.target.value = num.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
});
}
document.querySelectorAll('input[name="Tax_Exempt"]').forEach(radio => {
radio.addEventListener('change', () => {
checkRequiredFields();
@@ -495,6 +734,36 @@
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
// Enforce Job_Type and Job_Division selection
const jobType = (data.Job_Type || '').toString().trim();
const jobDivision = (data.Job_Division || '').toString().trim();
if (!jobType || !jobDivision) {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = 'Please select both Job Type and Job Division.';
message.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Job';
return;
}
// Enforce phone format (###) ###-####; reject otherwise
const phoneRaw = (data.Phone_Number || '').toString();
const isUnknown = phoneRaw.toLowerCase() === 'unknown';
if (!isUnknown) {
const digits = phoneRaw.replace(/\D/g, '').slice(0, 10);
if (digits.length === 10) {
const formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
data.Phone_Number = formatted;
} else {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = 'Phone number must be 10 digits or "Unknown". Add extensions in EXT field.';
message.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Job';
return;
}
}
// Add user's PocketBase token
data.pbToken = pb.authStore.token;
@@ -518,7 +787,49 @@
if (data.Start_Date) {
data.Start_Date = formatDateToShort(data.Start_Date);
}
// Address validation: require plausible address, coordinates, OR Address_Notes
const addr = (data.Job_Address || '').toString().trim();
const addrNotes = (data.Address_Notes || '').toString().trim();
// Check if it looks like coordinates: pattern "lat, lon" or "lat,lon"
const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
const isCoordinates = coordPattern.test(addr);
// Check if it looks like a real address: has digit(s) and letter(s), at least 5 chars
const hasDigit = /\d/.test(addr);
const hasAlpha = /[A-Za-z]/.test(addr);
const isAddress = addr.length >= 5 && hasDigit && hasAlpha;
const addressLooksValid = isCoordinates || isAddress;
if (!addressLooksValid && addrNotes.length < 5) {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = 'Provide a real address (e.g., 123 Main St, City, State), coordinates (e.g., 34.0522, -118.2437), or enter directions in Address Notes.';
message.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Job';
return;
}
data.Job_Address = addr;
data.Address_Notes = addrNotes;
// Job name validation: must be at least 4 characters, contain letters, not symbols-only, not "n/a"
const jobName = (data.Job_Name || '').toString().trim();
const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
const hasLetters = /[A-Za-z]/.test(jobName);
const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
const isLongEnough = jobName.length >= 4;
if (!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral) {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).';
message.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Job';
return;
}
data.Job_Name = jobName;
// Convert Tax_Exempt to boolean
if (data.Tax_Exempt) {
data.Tax_Exempt = data.Tax_Exempt === 'true';
@@ -553,6 +864,90 @@
submitBtn.textContent = 'Submit Job';
}
});
// Map Picker Functionality
let map = null;
let marker = null;
let selectedLatLng = null;
document.getElementById('openMapPicker').addEventListener('click', () => {
const modal = document.getElementById('mapPickerModal');
modal.classList.remove('hidden');
// Initialize map if not already done
if (!map) {
// Default to 1836 North Deffer Drive, Nixa, MO 65714
map = L.map('map').setView([37.0415, -93.2985], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
// Handle map clicks
map.on('click', (e) => {
const { lat, lng } = e.latlng;
// Remove existing marker if any
if (marker) {
map.removeLayer(marker);
}
// Add new marker
marker = L.marker([lat, lng]).addTo(map);
// Store coordinates
selectedLatLng = { lat, lng };
// Update display
document.getElementById('selectedCoords').textContent = `Selected: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
document.getElementById('confirmMapPicker').disabled = false;
});
}
// Invalidate size to ensure map renders correctly in modal
setTimeout(() => {
map.invalidateSize();
// If Job_Address already has coordinates, center on them
const currentAddr = document.getElementById('Job_Address').value.trim();
const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
if (coordPattern.test(currentAddr)) {
const [lat, lng] = currentAddr.split(',').map(s => parseFloat(s.trim()));
if (!isNaN(lat) && !isNaN(lng)) {
map.setView([lat, lng], 15);
if (marker) map.removeLayer(marker);
marker = L.marker([lat, lng]).addTo(map);
selectedLatLng = { lat, lng };
document.getElementById('selectedCoords').textContent = `Selected: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
document.getElementById('confirmMapPicker').disabled = false;
}
}
}, 100);
});
document.getElementById('closeMapPicker').addEventListener('click', () => {
document.getElementById('mapPickerModal').classList.add('hidden');
});
document.getElementById('cancelMapPicker').addEventListener('click', () => {
document.getElementById('mapPickerModal').classList.add('hidden');
});
document.getElementById('confirmMapPicker').addEventListener('click', () => {
if (selectedLatLng) {
const coordString = `${selectedLatLng.lat.toFixed(6)}, ${selectedLatLng.lng.toFixed(6)}`;
document.getElementById('Job_Address').value = coordString;
// Trigger blur validation
const addressInput = document.getElementById('Job_Address');
addressInput.classList.remove('border-red-500', 'bg-red-50');
addressInput.classList.add('border-green-500', 'bg-green-50');
addressInput.title = 'Valid coordinates format';
document.getElementById('mapPickerModal').classList.add('hidden');
}
});
</script>
<!-- Version Info -->
+48
View File
@@ -693,6 +693,54 @@ app.post('/api/submit', async (c) => {
if (dueIso) data.Due_Date = dueIso;
if (startIso) data.Start_Date = startIso;
// Address validation: require plausible address, coordinates, OR Address_Notes provided
const addr = (data.Job_Address || '').toString().trim();
const addrNotes = (data.Address_Notes || '').toString().trim();
// Check if it looks like coordinates: pattern "lat, lon" or "lat,lon"
const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
const isCoordinates = coordPattern.test(addr);
// Check if it looks like a real address: has digit(s) and letter(s), at least 5 chars
const hasDigit = /\d/.test(addr);
const hasAlpha = /[A-Za-z]/.test(addr);
const isAddress = addr.length >= 5 && hasDigit && hasAlpha;
const addressLooksValid = isCoordinates || isAddress;
if (!addressLooksValid && addrNotes.length < 5) {
return c.json({
success: false,
message: 'Provide a real address (e.g., 123 Main St, City, State), coordinates (e.g., 34.0522, -118.2437), or enter directions in Address Notes.'
}, 400);
}
data.Job_Address = addr;
data.Address_Notes = addrNotes;
// Job name validation: must be at least 4 characters, contain letters, not symbols-only, not "n/a"
const jobName = (data.Job_Name || '').toString().trim();
const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
const hasLetters = /[A-Za-z]/.test(jobName);
const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
const isLongEnough = jobName.length >= 4;
if (!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral) {
return c.json({
success: false,
message: 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).'
}, 400);
}
data.Job_Name = jobName;
// Enforce Job_Type and Job_Division selection
const jobType = (data.Job_Type || '').toString().trim();
const jobDivision = (data.Job_Division || '').toString().trim();
if (!jobType || !jobDivision) {
return c.json({
success: false,
message: 'Please select both Job Type and Job Division.'
}, 400);
}
// Get all existing job numbers and find the max
const existingJobs = await pb.collection(process.env.PB_COLLECTION!).getFullList({
fields: 'Job_Number',