|
|
|
@@ -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 = {
|
|
|
|
@@ -60,10 +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">Optional. Provide if known.</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">Optional. 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>
|
|
|
|
@@ -196,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>
|
|
|
|
|
|
|
|
|
@@ -234,8 +279,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 +309,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 +369,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 +404,25 @@
|
|
|
|
|
<p id="taxReminder" class="hidden mt-3 text-sm font-semibold text-red-700">Don't forget to file the paperwork!</p>
|
|
|
|
|
</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>
|
|
|
|
|
<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 +434,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>
|
|
|
|
@@ -402,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;
|
|
|
|
@@ -457,9 +590,74 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add event listeners to required fields
|
|
|
|
|
document.getElementById('Job_Name').addEventListener('input', checkRequiredFields);
|
|
|
|
|
document.getElementById('Office_Rep').addEventListener('change', checkRequiredFields);
|
|
|
|
|
|
|
|
|
|
// 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".`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// No validation on Job Address blur (optional)
|
|
|
|
|
document.getElementById('Office_Rep').addEventListener('change', updateSelectPlaceholders);
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
@@ -495,6 +693,24 @@
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
@@ -518,7 +734,12 @@
|
|
|
|
|
if (data.Start_Date) {
|
|
|
|
|
data.Start_Date = formatDateToShort(data.Start_Date);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
data.Tax_Exempt = data.Tax_Exempt === 'true';
|
|
|
|
@@ -553,11 +774,95 @@
|
|
|
|
|
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 -->
|
|
|
|
|
<div class="fixed bottom-4 right-4 text-xs text-gray-400">
|
|
|
|
|
v1.0.0-beta3
|
|
|
|
|
v1.0.0-beta3a
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|