Files
Job-Form/frontend/index.html
T

962 lines
47 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-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 = {
theme: {
extend: {
colors: {
primary: '#4c51bf',
secondary: '#5b21b6',
}
}
}
}
</script>
</head>
<body class="min-h-screen bg-gradient-to-br from-primary to-secondary p-4 sm:p-8 flex justify-center items-center font-sans">
<!-- Login Container -->
<div id="loginContainer" class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-8">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Cardoza Construction</h1>
<p class="text-gray-600">Job Creation Portal</p>
</div>
<div class="space-y-6">
<p class="text-gray-700 text-center">Sign in with your Microsoft account to access the Job Creation Form</p>
<button type="button" id="loginBtn" class="w-full py-3 px-4 bg-gradient-to-br from-primary to-secondary text-white font-semibold rounded-lg hover:opacity-95 transition-opacity shadow-lg hover:shadow-xl">
Login with Microsoft
</button>
</div>
<div id="loginError" class="hidden mt-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm"></div>
</div>
<!-- Form Container (hidden until logged in) -->
<div id="formContainer" class="hidden bg-white rounded-2xl shadow-2xl max-w-4xl w-full p-6 sm:p-12">
<div class="flex items-center justify-between mb-2">
<div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Job Creation Form</h1>
<p class="text-gray-600 text-sm sm:text-base">Please fill out all required fields to submit a new job</p>
</div>
<div class="text-right">
<p id="userEmailDisplay" class="text-sm text-gray-600 mb-2"></p>
<button type="button" id="logoutBtn" class="px-3 py-1 bg-gray-200 text-gray-800 rounded-lg text-sm hover:bg-gray-300 transition-colors">
Logout
</button>
</div>
</div>
<form id="jobForm" class="space-y-6" autocomplete="off">
<!-- Basic Information -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Basic Information</div>
<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>
</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>
<div class="grid grid-cols-1 gap-6 mb-6">
<div class="flex flex-col">
<label for="Job_Status" class="block text-sm font-medium text-gray-700 mb-2">Job Status</label>
<select id="Job_Status" name="Job_Status" required 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 bg-white text-gray-900">
<option value="Estimating" selected>Estimating</option>
<option value="Est Sent">Est Sent</option>
<option value="Est Hold">Est Hold</option>
<option value="Follow Up">Follow Up</option>
<option value="Awarded">Awarded</option>
<option value="In Progress">In Progress</option>
<option value="On Hold">On Hold</option>
<option value="Billed (In Progress)">Billed (In Progress)</option>
<option value="Billed (Closed)">Billed (Closed)</option>
<option value="Archive">Archive</option>
<option value="Not Awarded">Not Awarded</option>
<option value="Not Bidding">Not Bidding</option>
</select>
</div>
</div>
<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">Job Type</label>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypeFX" name="Job_Type" value="FX" class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm">FX</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypeTM" name="Job_Type" value="TM" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">TM</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypeUP" name="Job_Type" value="UP" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">UP</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypePW" name="Job_Type" value="PW" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">PW</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypeFXEX" name="Job_Type" value="FXEX" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">FXEX</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="jobTypePWEX" name="Job_Type" value="PWEX" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">PWEX</span>
</label>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-col">
<label class="block text-sm font-medium text-gray-700 mb-2">Job Division</label>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="divisionC" name="Job_Division" value="C#" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">C#</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="divisionR" name="Job_Division" value="R#" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">R#</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="divisionS" name="Job_Division" value="S#" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">S#</span>
</label>
</div>
</div>
</div>
</div>
<!-- Company & Contact -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Company & Contact Information</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="Company_Client" class="block text-sm font-medium text-gray-700 mb-2">Company / Client</label>
<input type="text" id="Company_Client" name="Company_Client" placeholder="Enter company or client name" autocomplete="organization"
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="Contact_Person" class="block text-sm font-medium text-gray-700 mb-2">Contact Person</label>
<input type="text" id="Contact_Person" name="Contact_Person" placeholder="Enter contact person" autocomplete="name"
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 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="(###) ###-####" 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">
<label for="Email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input type="email" id="Email" name="Email" placeholder="Enter email address" autocomplete="email"
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 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>
<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>
<!-- Personnel -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Personnel & Estimator</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
<div class="flex flex-col">
<label for="Office_Rep" class="block text-sm font-medium text-gray-700 mb-2">
Office Rep <span class="text-red-600">*</span>
</label>
<select id="Office_Rep" name="Office_Rep" required 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 bg-white text-gray-400" data-placeholder="true">
<option value="" disabled selected>Select office rep</option>
<option value="Beth Cardoza">Beth Cardoza</option>
<option value="Steve Brewer">Steve Brewer</option>
</select>
</div>
<div class="flex flex-col">
<label for="Estimator" class="block text-sm font-medium text-gray-700 mb-2">Estimator</label>
<select id="Estimator" name="Estimator" required 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 bg-white text-gray-400" data-placeholder="true">
<option value="" disabled selected>Select estimator</option>
<option value="Beth Cardoza">Beth Cardoza</option>
<option value="Steve Brewer">Steve Brewer</option>
<option value="Timothy Cardoza">Timothy Cardoza</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="flex flex-col">
<label for="Project_Manager" class="block text-sm font-medium text-gray-700 mb-2">Project Manager</label>
<select id="Project_Manager" name="Project_Manager" required 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 bg-white text-gray-400" data-placeholder="true">
<option value="" disabled selected>Select project manager</option>
<option value="Dave">Dave</option>
<option value="Rick">Rick</option>
<option value="Eddie">Eddie</option>
<option value="Steve">Steve</option>
<option value="Timothy">Timothy</option>
</select>
</div>
</div>
</div>
<!-- Schedule & Dates -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Schedule & Dates</div>
<!-- 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" 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>
<!-- Schedule Confidence (now second) -->
<div class="grid grid-cols-1 gap-6 mb-6">
<div class="flex flex-col">
<label for="Schedule_Confidence" class="block text-sm font-medium text-gray-700 mb-2">Schedule Confidence</label>
<select id="Schedule_Confidence" name="Schedule_Confidence" required 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 bg-white text-gray-400" data-placeholder="true">
<option value="" disabled selected>Select confidence level</option>
<option value="1 (Total Guess)">1 (Total Guess)</option>
<option value="2 (Guess)">2 (Guess)</option>
<option value="3 (Start Date)">3 (Start Date)</option>
<option value="4 (Client Verbal Guess)">4 (Client Verbal Guess)</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7 (Written Schedule Within 90 Days)">7 (Written Schedule Within 90 Days)</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10 (Job Starts Within 7 Days)">10 (Job Starts Within 7 Days)</option>
</select>
</div>
</div>
<!-- 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" 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" 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 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">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="sourceRequested" name="Due_Date_Source" value="Requested" class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm">Requested</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="sourceGuess" name="Due_Date_Source" value="Guess" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">Guess</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="sourceStartDate" name="Due_Date_Source" value="Start Date" class="w-4 h-4 text-primary focus:ring-primary">
<span class="text-sm">Start Date</span>
</label>
</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>
<!-- Financial -->
<div id="financialSection" class="bg-red-100 border border-red-200 rounded-xl p-4 sm:p-6 transition-colors">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Financial</div>
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-col">
<label class="block text-sm font-medium text-gray-700 mb-2">
Tax Exempt <span class="text-red-600">*</span>
</label>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="taxExemptTrue" name="Tax_Exempt" value="true" required class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm">Yes</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" id="taxExemptFalse" name="Tax_Exempt" value="false" required class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm">No</span>
</label>
</div>
<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
class="w-full py-3 bg-gradient-to-br from-primary to-secondary text-white font-semibold rounded-lg hover:opacity-95 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl">
Submit Job
</button>
<div id="message" class="hidden p-4 rounded-lg font-medium"></div>
</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>
// Initialize PocketBase client
const pb = new PocketBase('https://pocketbase.ccllc.pro');
// Update auth UI based on login state
function updateAuthUI() {
const loginContainer = document.getElementById('loginContainer');
const formContainer = document.getElementById('formContainer');
const userEmailDisplay = document.getElementById('userEmailDisplay');
if (pb.authStore.isValid) {
// Show form, hide login
loginContainer.classList.add('hidden');
formContainer.classList.remove('hidden');
const userName = pb.authStore.record?.name || pb.authStore.record?.email || 'Unknown User';
userEmailDisplay.textContent = userName;
} else {
// Show login, hide form
loginContainer.classList.remove('hidden');
formContainer.classList.add('hidden');
document.getElementById('loginError').classList.add('hidden');
}
}
// Handle login button click
document.getElementById('loginBtn').addEventListener('click', async (e) => {
e.preventDefault();
const loginBtn = document.getElementById('loginBtn');
const loginError = document.getElementById('loginError');
loginBtn.disabled = true;
loginBtn.textContent = 'Logging in...';
loginError.classList.add('hidden');
try {
const authData = await pb.collection('Users').authWithOAuth2({
provider: 'microsoft',
});
console.log('✓ Logged in with Microsoft');
updateAuthUI();
} catch (error) {
console.error(error);
loginError.textContent = `Login failed: ${error.message}`;
loginError.classList.remove('hidden');
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Login with Microsoft';
}
});
// Handle logout button click
document.getElementById('logoutBtn').addEventListener('click', () => {
pb.authStore.clear();
updateAuthUI();
});
// Initial auth UI update
updateAuthUI();
// Initialize date pickers
flatpickr('.datepicker', {
dateFormat: 'm/d/Y',
allowInput: true,
altInput: true,
altFormat: 'm/d/Y',
});
// 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) {
submitBtn.disabled = false;
} else {
submitBtn.disabled = true;
}
}
// Update Financial section background and reminder based on Tax Exempt selection
function updateFinancialUI() {
const section = document.getElementById('financialSection');
const reminder = document.getElementById('taxReminder');
const selection = document.querySelector('input[name="Tax_Exempt"]:checked');
// Default: light red, hide reminder
section.classList.remove('bg-green-50', 'border-green-200', 'border-red-300');
section.classList.add('bg-red-100', 'border-red-200');
reminder.classList.add('hidden');
if (!selection) {
return;
}
if (selection.value === 'false') {
// No selected: light green
section.classList.remove('bg-red-100', 'border-red-200', 'border-red-300');
section.classList.add('bg-green-50', 'border-green-200');
reminder.classList.add('hidden');
} else {
// Yes selected: stay red and show reminder
section.classList.remove('bg-green-50', 'border-green-200');
section.classList.add('bg-red-100', 'border-red-300');
reminder.classList.remove('hidden');
}
}
// Style selects to show gray placeholder until a real value is chosen
function updateSelectPlaceholders() {
const selects = document.querySelectorAll('select[data-placeholder="true"]');
selects.forEach((sel) => {
if (!sel.value) {
sel.classList.add('text-gray-400');
sel.classList.remove('text-gray-900');
} else {
sel.classList.remove('text-gray-400');
sel.classList.add('text-gray-900');
}
});
}
// 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();
updateFinancialUI();
});
});
document.getElementById('Estimator').addEventListener('change', updateSelectPlaceholders);
document.getElementById('Project_Manager').addEventListener('change', updateSelectPlaceholders);
document.getElementById('Schedule_Confidence').addEventListener('change', updateSelectPlaceholders);
// Initial check
checkRequiredFields();
updateFinancialUI();
updateSelectPlaceholders();
// Form submission
document.getElementById('jobForm').addEventListener('submit', async (e) => {
e.preventDefault();
// Check if user is authenticated
if (!pb.authStore.isValid) {
alert('Please log in with your Microsoft account first');
return;
}
const submitBtn = e.target.querySelector('#submitBtn');
const message = document.getElementById('message');
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
message.classList.add('hidden');
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;
// Convert dates to MM/dd/yyyy format for PocketBase
function formatDateToShort(dateString) {
if (!dateString) return '';
const parts = dateString.split('/');
if (parts.length === 3) {
const month = parts[0].padStart(2, '0');
const day = parts[1].padStart(2, '0');
const year = parts[2];
return `${month}/${day}/${year}`;
}
return dateString;
}
if (data.Due_Date) {
data.Due_Date = formatDateToShort(data.Due_Date);
}
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';
}
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.success) {
message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200';
message.textContent = result.message;
message.classList.remove('hidden');
e.target.reset();
checkRequiredFields();
} else {
throw new Error(result.message);
}
} catch (error) {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = error.message || 'Failed to submit form. Please try again.';
message.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
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
</div>
</body>
</html>