|
|
|
@@ -62,11 +62,11 @@
|
|
|
|
|
<div class="grid grid-cols-1 gap-6 mb-6">
|
|
|
|
|
<div class="flex flex-col">
|
|
|
|
|
<label for="Job_Name" class="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
Job Name <span class="text-red-600">*</span>
|
|
|
|
|
Job Name
|
|
|
|
|
</label>
|
|
|
|
|
<input type="text" id="Job_Name" name="Job_Name" required placeholder="Enter job name" autocomplete="off"
|
|
|
|
|
<input type="text" id="Job_Name" name="Job_Name" 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>
|
|
|
|
|
<p class="text-xs text-gray-500 mt-1">Optional. Provide if known.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -167,7 +167,7 @@
|
|
|
|
|
<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="(###) ###-####" 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>
|
|
|
|
|
<p class="text-xs text-gray-500 mt-1">Optional. Format: (###) ###-####. Extension can go in EXT.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
|
@@ -240,6 +240,7 @@
|
|
|
|
|
<option value="" disabled selected>Select office rep</option>
|
|
|
|
|
<option value="Beth Cardoza">Beth Cardoza</option>
|
|
|
|
|
<option value="Steve Brewer">Steve Brewer</option>
|
|
|
|
|
<option value="Timothy Cardoza">Timothy Cardoza</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -404,6 +405,10 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- FUTURE: Job Size Guess and Guess Source
|
|
|
|
|
To be enabled in future: allow sq ft, linear ft, and/or dollar amount
|
|
|
|
|
Job_Size_Guess: Basic financial cost/value of the job
|
|
|
|
|
Job_Size_Guess_Source: Who provided the guess (Estimator, Rick, Project Manager, etc.)
|
|
|
|
|
<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>
|
|
|
|
@@ -417,6 +422,7 @@
|
|
|
|
|
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
|
|
|
|
@@ -530,12 +536,11 @@
|
|
|
|
|
|
|
|
|
|
// Function to check if required fields are filled
|
|
|
|
|
function checkRequiredFields() {
|
|
|
|
|
const jobName = document.getElementById('Job_Name').value.trim();
|
|
|
|
|
const officeRep = document.getElementById('Office_Rep').value;
|
|
|
|
|
const taxExempt = document.querySelector('input[name="Tax_Exempt"]:checked');
|
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
|
|
|
|
|
|
if (jobName && officeRep && taxExempt) {
|
|
|
|
|
if (officeRep && taxExempt) {
|
|
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
submitBtn.disabled = true;
|
|
|
|
@@ -585,27 +590,8 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
@@ -656,49 +642,22 @@
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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.';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// No validation on Job Address blur (optional)
|
|
|
|
|
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 });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// FUTURE: Job Size Guess formatter (when field is re-enabled)
|
|
|
|
|
// const jobSizeInput = document.getElementById('Job_Size_Guess');
|
|
|
|
|
// if (jobSizeInput) {
|
|
|
|
|
// jobSizeInput.addEventListener('blur', (e) => {
|
|
|
|
|
// const raw = (e.target.value || '').toString().trim();
|
|
|
|
|
// if (!raw) return;
|
|
|
|
|
// 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();
|
|
|
|
@@ -747,23 +706,11 @@
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// FUTURE: Phone number format coercion
|
|
|
|
|
// When re-enabled, enforce strict format: (###) ###-#### or "Unknown" (or blank to allow)
|
|
|
|
|
// Currently: Phone is optional, no validation on submit
|
|
|
|
|
const phoneRaw = (data.Phone_Number || '').toString().trim();
|
|
|
|
|
data.Phone_Number = phoneRaw; // Accept as-is if provided, or empty if blank
|
|
|
|
|
|
|
|
|
|
// Add user's PocketBase token
|
|
|
|
|
data.pbToken = pb.authStore.token;
|
|
|
|
@@ -788,47 +735,10 @@
|
|
|
|
|
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;
|
|
|
|
|
// Address and Job Name: optional, no validation
|
|
|
|
|
data.Job_Address = (data.Job_Address || '').toString().trim();
|
|
|
|
|
data.Address_Notes = (data.Address_Notes || '').toString().trim();
|
|
|
|
|
data.Job_Name = (data.Job_Name || '').toString().trim();
|
|
|
|
|
|
|
|
|
|
// Convert Tax_Exempt to boolean
|
|
|
|
|
if (data.Tax_Exempt) {
|
|
|
|
@@ -952,7 +862,7 @@
|
|
|
|
|
|
|
|
|
|
<!-- Version Info -->
|
|
|
|
|
<div class="fixed bottom-4 right-4 text-xs text-gray-400">
|
|
|
|
|
v1.0.0-beta3
|
|
|
|
|
v1.0.0-beta3a
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|