diff --git a/CHANGELOG.md b/CHANGELOG.md
index 872cc0b..6e9a5aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file.
- Ops
- Systemd service now uses `EnvironmentFile` for production env vars; fixed `EnvironmentFile` typo and validated permissions.
- Switched service restart policy to `Restart=always` for resilience.
- - Confirmed service listens on `PORT` (default 4000) and added guidance for restarts after env changes.
+ - Confirmed service listens on `PORT` (default 3020) and added guidance for restarts after env changes.
- Excel Sync
- Addressed column mismatch errors by refreshing Excel table column cache on service restart; sync confirmed working.
- Versioning
diff --git a/README.md b/README.md
index f984920..841dbd3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Job Creation Form with Excel Sync
-> Current Version: v1.0.0-beta2
+> Current Version: v1.0.0-beta3a
See CHANGELOG.md for release notes.
@@ -48,7 +48,7 @@ ITEM_ID=01SPNXLDQRICHB63BFUNGKZ3GE6RL7LGBT
EXCEL_TABLE=/drives/.../tables/Test_Table
# Server
-PORT=4000
+PORT=3020
```
## Installation
@@ -67,7 +67,7 @@ Start the server:
bun run dev
```
-Server runs at: http://localhost:4000
+Server runs at: http://localhost:3020
## Key Features
diff --git a/frontend/index.html b/frontend/index.html
index 5202d10..4e5df1d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -62,11 +62,11 @@
- Job Name *
+ Job Name
-
-
Use a descriptive name (no symbols-only, no N/A).
+
Optional. Provide if known.
@@ -167,7 +167,7 @@
Phone Number
- Use format (###) ###-####. Extension can go in EXT.
+ Optional. Format: (###) ###-####. Extension can go in EXT.
@@ -240,6 +240,7 @@
Select office rep
Beth Cardoza
Steve Brewer
+ Timothy Cardoza
@@ -404,6 +405,10 @@
+
{
- const jobName = (e.target.value || '').toString().trim();
- const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
- const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
- const hasLetters = /[A-Za-z]/.test(jobName);
- const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
- const isLongEnough = jobName.length >= 4;
-
- if (jobName.length > 0 && (!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral)) {
- e.target.classList.add('border-red-500', 'bg-red-50');
- e.target.title = 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).';
- } else {
- e.target.classList.remove('border-red-500', 'bg-red-50');
- e.target.title = '';
- }
- });
-
// Phone number field: validate and format on blur
document.getElementById('Phone_Number').addEventListener('blur', (e) => {
const phoneRaw = (e.target.value || '').toString().trim();
@@ -656,49 +642,22 @@
}
});
- // Job Address field: validate address or coordinates on blur
- document.getElementById('Job_Address').addEventListener('blur', (e) => {
- const addr = (e.target.value || '').toString().trim();
- if (!addr) {
- e.target.classList.remove('border-red-500', 'bg-red-50', 'border-green-500', 'bg-green-50');
- e.target.title = '';
- return; // Empty is allowed, will check Address_Notes on submit
- }
-
- // Check if it looks like coordinates: pattern "lat, lon" or "lat,lon"
- const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
- const isCoordinates = coordPattern.test(addr);
-
- // Check if it looks like a real address: has digit(s) and letter(s), at least 5 chars
- const hasDigit = /\d/.test(addr);
- const hasAlpha = /[A-Za-z]/.test(addr);
- const isAddress = addr.length >= 5 && hasDigit && hasAlpha;
-
- if (isCoordinates || isAddress) {
- e.target.classList.remove('border-red-500', 'bg-red-50');
- e.target.classList.add('border-green-500', 'bg-green-50');
- e.target.title = isCoordinates ? 'Valid coordinates format' : 'Valid address format';
- } else {
- e.target.classList.remove('border-green-500', 'bg-green-50');
- e.target.classList.add('border-red-500', 'bg-red-50');
- e.target.title = 'Address must be like "123 Main St, City, State" or coordinates like "34.0522, -118.2437". Add directions in Address Notes if needed.';
- }
- });
+ // No validation on Job Address blur (optional)
document.getElementById('Office_Rep').addEventListener('change', updateSelectPlaceholders);
- // Job Size Guess: format as USD on blur
- const jobSizeInput = document.getElementById('Job_Size_Guess');
- if (jobSizeInput) {
- jobSizeInput.addEventListener('blur', (e) => {
- const raw = (e.target.value || '').toString().trim();
- if (!raw) return;
- // Keep only digits and one decimal point
- const cleaned = raw.replace(/[^0-9.]/g, '').replace(/(\..*?)\./g, '$1');
- const num = parseFloat(cleaned);
- if (isNaN(num)) return;
- e.target.value = num.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
- });
- }
+ // FUTURE: Job Size Guess formatter (when field is re-enabled)
+ // const jobSizeInput = document.getElementById('Job_Size_Guess');
+ // if (jobSizeInput) {
+ // jobSizeInput.addEventListener('blur', (e) => {
+ // const raw = (e.target.value || '').toString().trim();
+ // if (!raw) return;
+ // const cleaned = raw.replace(/[^0-9.]/g, '').replace(/(\..*?)\./g, '$1');
+ // const num = parseFloat(cleaned);
+ // if (isNaN(num)) return;
+ // e.target.value = num.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ // });
+ // }
+
document.querySelectorAll('input[name="Tax_Exempt"]').forEach(radio => {
radio.addEventListener('change', () => {
checkRequiredFields();
@@ -747,23 +706,11 @@
return;
}
- // Enforce phone format (###) ###-####; reject otherwise
- const phoneRaw = (data.Phone_Number || '').toString();
- const isUnknown = phoneRaw.toLowerCase() === 'unknown';
- if (!isUnknown) {
- const digits = phoneRaw.replace(/\D/g, '').slice(0, 10);
- if (digits.length === 10) {
- const formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
- data.Phone_Number = formatted;
- } else {
- message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
- message.textContent = 'Phone number must be 10 digits or "Unknown". Add extensions in EXT field.';
- message.classList.remove('hidden');
- submitBtn.disabled = false;
- submitBtn.textContent = 'Submit Job';
- return;
- }
- }
+ // FUTURE: Phone number format coercion
+ // When re-enabled, enforce strict format: (###) ###-#### or "Unknown" (or blank to allow)
+ // Currently: Phone is optional, no validation on submit
+ const phoneRaw = (data.Phone_Number || '').toString().trim();
+ data.Phone_Number = phoneRaw; // Accept as-is if provided, or empty if blank
// Add user's PocketBase token
data.pbToken = pb.authStore.token;
@@ -788,47 +735,10 @@
data.Start_Date = formatDateToShort(data.Start_Date);
}
- // Address validation: require plausible address, coordinates, OR Address_Notes
- const addr = (data.Job_Address || '').toString().trim();
- const addrNotes = (data.Address_Notes || '').toString().trim();
-
- // Check if it looks like coordinates: pattern "lat, lon" or "lat,lon"
- const coordPattern = /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/;
- const isCoordinates = coordPattern.test(addr);
-
- // Check if it looks like a real address: has digit(s) and letter(s), at least 5 chars
- const hasDigit = /\d/.test(addr);
- const hasAlpha = /[A-Za-z]/.test(addr);
- const isAddress = addr.length >= 5 && hasDigit && hasAlpha;
-
- const addressLooksValid = isCoordinates || isAddress;
- if (!addressLooksValid && addrNotes.length < 5) {
- message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
- message.textContent = 'Provide a real address (e.g., 123 Main St, City, State), coordinates (e.g., 34.0522, -118.2437), or enter directions in Address Notes.';
- message.classList.remove('hidden');
- submitBtn.disabled = false;
- submitBtn.textContent = 'Submit Job';
- return;
- }
- data.Job_Address = addr;
- data.Address_Notes = addrNotes;
-
- // Job name validation: must be at least 4 characters, contain letters, not symbols-only, not "n/a"
- const jobName = (data.Job_Name || '').toString().trim();
- const invalidLiterals = ['n/a', 'na', 'n.a.', 'n\\a', 'none'];
- const onlySymbols = /^[^A-Za-z0-9]+$/.test(jobName);
- const hasLetters = /[A-Za-z]/.test(jobName);
- const isInvalidLiteral = invalidLiterals.includes(jobName.toLowerCase());
- const isLongEnough = jobName.length >= 4;
- if (!isLongEnough || !hasLetters || onlySymbols || isInvalidLiteral) {
- message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
- message.textContent = 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).';
- message.classList.remove('hidden');
- submitBtn.disabled = false;
- submitBtn.textContent = 'Submit Job';
- return;
- }
- data.Job_Name = jobName;
+ // Address and Job Name: optional, no validation
+ data.Job_Address = (data.Job_Address || '').toString().trim();
+ data.Address_Notes = (data.Address_Notes || '').toString().trim();
+ data.Job_Name = (data.Job_Name || '').toString().trim();
// Convert Tax_Exempt to boolean
if (data.Tax_Exempt) {
@@ -952,7 +862,7 @@
- v1.0.0-beta3
+ v1.0.0-beta3a