|
|
|
@@ -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 = {
|
|
|
|
@@ -62,8 +64,9 @@
|
|
|
|
|
<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"
|
|
|
|
|
<input type="text" id="Job_Name" name="Job_Name" required placeholder="Enter job name" autocomplete="off" value="unk"
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
@@ -93,7 +96,7 @@
|
|
|
|
|
<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">
|
|
|
|
|
<input type="radio" id="jobTypeFX" name="Job_Type" value="FX" class="w-4 h-4 text-primary focus:ring-primary" checked autocomplete="off">
|
|
|
|
|
<span class="text-sm">FX</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
@@ -129,7 +132,7 @@
|
|
|
|
|
<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">
|
|
|
|
|
<input type="radio" id="divisionR" name="Job_Division" value="R#" class="w-4 h-4 text-primary focus:ring-primary" checked>
|
|
|
|
|
<span class="text-sm">R#</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
@@ -148,13 +151,13 @@
|
|
|
|
|
<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"
|
|
|
|
|
<input type="text" id="Company_Client" name="Company_Client" placeholder="Enter company or client name" autocomplete="organization" value="Tester"
|
|
|
|
|
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"
|
|
|
|
|
<input type="text" id="Contact_Person" name="Contact_Person" placeholder="Enter contact person" autocomplete="name" value="Tester"
|
|
|
|
|
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>
|
|
|
|
@@ -162,22 +165,63 @@
|
|
|
|
|
<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" value="(111) 111-1111"
|
|
|
|
|
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"
|
|
|
|
|
<input type="email" id="Email" name="Email" placeholder="Enter email address" autocomplete="email" value="test@test"
|
|
|
|
|
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" value="123"
|
|
|
|
|
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" value="(222) 222-2222"
|
|
|
|
|
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">testing</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" value="123 Main St. Los Angeles, CA"
|
|
|
|
|
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>
|
|
|
|
@@ -193,8 +237,8 @@
|
|
|
|
|
</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="" disabled>Select office rep</option>
|
|
|
|
|
<option value="Beth Cardoza" selected>Beth Cardoza</option>
|
|
|
|
|
<option value="Steve Brewer">Steve Brewer</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
@@ -203,8 +247,8 @@
|
|
|
|
|
<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="" disabled>Select estimator</option>
|
|
|
|
|
<option value="Beth Cardoza" selected>Beth Cardoza</option>
|
|
|
|
|
<option value="Steve Brewer">Steve Brewer</option>
|
|
|
|
|
<option value="Timothy Cardoza">Timothy Cardoza</option>
|
|
|
|
|
</select>
|
|
|
|
@@ -216,9 +260,9 @@
|
|
|
|
|
<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="" disabled>Select project manager</option>
|
|
|
|
|
<option value="Dave">Dave</option>
|
|
|
|
|
<option value="Rick">Rick</option>
|
|
|
|
|
<option value="Rick" selected>Rick</option>
|
|
|
|
|
<option value="Eddie">Eddie</option>
|
|
|
|
|
<option value="Steve">Steve</option>
|
|
|
|
|
<option value="Timothy">Timothy</option>
|
|
|
|
@@ -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>
|
|
|
|
|
<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">
|
|
|
|
|
<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" value="1/1/26">
|
|
|
|
|
<p class="text-xs text-gray-500 mt-1">Estimated date work will start.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -245,10 +290,10 @@
|
|
|
|
|
<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="" disabled>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="3 (Start Date)" selected>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>
|
|
|
|
@@ -263,24 +308,53 @@
|
|
|
|
|
<!-- 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>
|
|
|
|
|
<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">
|
|
|
|
|
<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" value="1/6/26">
|
|
|
|
|
<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" value="1:00 PM"
|
|
|
|
|
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">
|
|
|
|
|
<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">
|
|
|
|
|
<input type="radio" id="sourceRequested" name="Due_Date_Source" value="Requested" class="w-4 h-4 text-primary focus:ring-primary" checked autocomplete="off">
|
|
|
|
|
<span class="text-sm">Requested</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
@@ -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">testing notes</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -308,18 +391,32 @@
|
|
|
|
|
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="taxExemptFalse" name="Tax_Exempt" value="false" required class="w-4 h-4 text-primary focus:ring-primary" checked autocomplete="off">
|
|
|
|
|
<span class="text-sm">No</span>
|
|
|
|
|
</label>
|
|
|
|
|
<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)" value="1"
|
|
|
|
|
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." value="me"
|
|
|
|
|
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,120 @@
|
|
|
|
|
// 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 allowedSpecials = ['unk', 'unknown'];
|
|
|
|
|
const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
|
|
|
|
|
const hasLetters = /[A-Za-z]/.test(jobName);
|
|
|
|
|
const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
|
|
|
|
|
const isAllowedSpecial = allowedSpecials.includes(jobName.toLowerCase());
|
|
|
|
|
const isLongEnough = jobName.length >= 4;
|
|
|
|
|
|
|
|
|
|
if (jobName.length > 0 && ((!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral) && !isAllowedSpecial)) {
|
|
|
|
|
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, but UNK/UNKNOWN allowed).';
|
|
|
|
|
} 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();
|
|
|
|
@@ -496,6 +737,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;
|
|
|
|
|
|
|
|
|
@@ -519,13 +790,57 @@
|
|
|
|
|
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" (but allow "unk"/"unknown")
|
|
|
|
|
const jobName = (data.Job_Name || '').toString().trim();
|
|
|
|
|
const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
|
|
|
|
|
const allowedSpecials = ['unk', 'unknown'];
|
|
|
|
|
const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
|
|
|
|
|
const hasLetters = /[A-Za-z]/.test(jobName);
|
|
|
|
|
const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
|
|
|
|
|
const isAllowedSpecial = allowedSpecials.includes(jobName.toLowerCase());
|
|
|
|
|
const isLongEnough = jobName.length >= 4;
|
|
|
|
|
if ((!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral) && !isAllowedSpecial) {
|
|
|
|
|
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, but UNK/UNKNOWN allowed).';
|
|
|
|
|
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', {
|
|
|
|
|
const response = await fetch('http://localhost:3020/api/submit', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
@@ -553,12 +868,122 @@
|
|
|
|
|
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 class="fixed bottom-4 right-4 text-xs text-gray-400 flex items-center gap-2">
|
|
|
|
|
<span id="env-badge" class="hidden px-1.5 py-0.5 rounded-sm font-semibold uppercase tracking-wider text-[10px]"></span>
|
|
|
|
|
<span id="version-text">v1.0.0-beta3</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Fetch environment info
|
|
|
|
|
async function updateEnvBadge() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/config');
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
const badge = document.getElementById('env-badge');
|
|
|
|
|
|
|
|
|
|
if (data.mode === 'PROD') {
|
|
|
|
|
badge.textContent = 'PROD';
|
|
|
|
|
badge.className = 'px-1.5 py-0.5 rounded-sm font-bold uppercase tracking-wider text-[10px] bg-green-600 text-white';
|
|
|
|
|
badge.classList.remove('hidden');
|
|
|
|
|
} else if (data.mode === 'TEST') {
|
|
|
|
|
badge.textContent = 'TEST';
|
|
|
|
|
badge.className = 'px-1.5 py-0.5 rounded-sm font-bold uppercase tracking-wider text-[10px] bg-amber-500 text-white';
|
|
|
|
|
badge.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to fetch config:', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
updateEnvBadge();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
|
|
|
|
|