Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7cb428ed7 |
+1
-1
@@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Ops
|
- Ops
|
||||||
- Systemd service now uses `EnvironmentFile` for production env vars; fixed `EnvironmentFile` typo and validated permissions.
|
- Systemd service now uses `EnvironmentFile` for production env vars; fixed `EnvironmentFile` typo and validated permissions.
|
||||||
- Switched service restart policy to `Restart=always` for resilience.
|
- 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
|
- Excel Sync
|
||||||
- Addressed column mismatch errors by refreshing Excel table column cache on service restart; sync confirmed working.
|
- Addressed column mismatch errors by refreshing Excel table column cache on service restart; sync confirmed working.
|
||||||
- Versioning
|
- Versioning
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Job Creation Form with Excel Sync
|
# Job Creation Form with Excel Sync
|
||||||
|
|
||||||
> Current Version: v1.0.0-beta2
|
> Current Version: v1.0.0-beta3a
|
||||||
|
|
||||||
See CHANGELOG.md for release notes.
|
See CHANGELOG.md for release notes.
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ ITEM_ID=01SPNXLDQRICHB63BFUNGKZ3GE6RL7LGBT
|
|||||||
EXCEL_TABLE=/drives/.../tables/Test_Table
|
EXCEL_TABLE=/drives/.../tables/Test_Table
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=3020
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -67,7 +67,7 @@ Start the server:
|
|||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Server runs at: http://localhost:4000
|
Server runs at: http://localhost:3020
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
|
|||||||
+35
-125
@@ -62,11 +62,11 @@
|
|||||||
<div class="grid grid-cols-1 gap-6 mb-6">
|
<div class="grid grid-cols-1 gap-6 mb-6">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label for="Job_Name" class="block text-sm font-medium text-gray-700 mb-2">
|
<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>
|
</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">
|
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
|
||||||
<p class="text-xs text-gray-500 mt-1">Use a descriptive name (no symbols-only, no N/A).</p>
|
<p class="text-xs text-gray-500 mt-1">Optional. Provide if known.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<label for="Phone_Number" class="block text-sm font-medium text-gray-700 mb-2">Phone Number</label>
|
<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"
|
<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">
|
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
|
||||||
<p class="text-xs text-gray-500 mt-1">Use format (###) ###-####. Extension can go in EXT.</p>
|
<p class="text-xs text-gray-500 mt-1">Optional. Format: (###) ###-####. Extension can go in EXT.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -240,6 +240,7 @@
|
|||||||
<option value="" disabled selected>Select office rep</option>
|
<option value="" disabled selected>Select office rep</option>
|
||||||
<option value="Beth Cardoza">Beth Cardoza</option>
|
<option value="Beth Cardoza">Beth Cardoza</option>
|
||||||
<option value="Steve Brewer">Steve Brewer</option>
|
<option value="Steve Brewer">Steve Brewer</option>
|
||||||
|
<option value="Timothy Cardoza">Timothy Cardoza</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -404,6 +405,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="grid grid-cols-1 sm:grid-cols-2 gap-6 mt-6">
|
||||||
<div class="flex flex-col">
|
<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>
|
<label for="Job_Size_Guess" class="block text-sm font-medium text-gray-700 mb-2" title="Basic financial cost/value of the job. Example: $25,000.00 (just a guess).">Job Size Guess</label>
|
||||||
@@ -417,6 +422,7 @@
|
|||||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
|
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>
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" id="submitBtn" disabled
|
<button type="submit" id="submitBtn" disabled
|
||||||
@@ -530,12 +536,11 @@
|
|||||||
|
|
||||||
// Function to check if required fields are filled
|
// Function to check if required fields are filled
|
||||||
function checkRequiredFields() {
|
function checkRequiredFields() {
|
||||||
const jobName = document.getElementById('Job_Name').value.trim();
|
|
||||||
const officeRep = document.getElementById('Office_Rep').value;
|
const officeRep = document.getElementById('Office_Rep').value;
|
||||||
const taxExempt = document.querySelector('input[name="Tax_Exempt"]:checked');
|
const taxExempt = document.querySelector('input[name="Tax_Exempt"]:checked');
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
|
||||||
if (jobName && officeRep && taxExempt) {
|
if (officeRep && taxExempt) {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
@@ -585,27 +590,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners to required fields
|
// Add event listeners to required fields
|
||||||
document.getElementById('Job_Name').addEventListener('input', checkRequiredFields);
|
|
||||||
document.getElementById('Office_Rep').addEventListener('change', 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
|
// Phone number field: validate and format on blur
|
||||||
document.getElementById('Phone_Number').addEventListener('blur', (e) => {
|
document.getElementById('Phone_Number').addEventListener('blur', (e) => {
|
||||||
const phoneRaw = (e.target.value || '').toString().trim();
|
const phoneRaw = (e.target.value || '').toString().trim();
|
||||||
@@ -656,49 +642,22 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Job Address field: validate address or coordinates on blur
|
// No validation on Job Address blur (optional)
|
||||||
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);
|
document.getElementById('Office_Rep').addEventListener('change', updateSelectPlaceholders);
|
||||||
|
|
||||||
// Job Size Guess: format as USD on blur
|
// FUTURE: Job Size Guess formatter (when field is re-enabled)
|
||||||
const jobSizeInput = document.getElementById('Job_Size_Guess');
|
// const jobSizeInput = document.getElementById('Job_Size_Guess');
|
||||||
if (jobSizeInput) {
|
// if (jobSizeInput) {
|
||||||
jobSizeInput.addEventListener('blur', (e) => {
|
// jobSizeInput.addEventListener('blur', (e) => {
|
||||||
const raw = (e.target.value || '').toString().trim();
|
// const raw = (e.target.value || '').toString().trim();
|
||||||
if (!raw) return;
|
// if (!raw) return;
|
||||||
// Keep only digits and one decimal point
|
// const cleaned = raw.replace(/[^0-9.]/g, '').replace(/(\..*?)\./g, '$1');
|
||||||
const cleaned = raw.replace(/[^0-9.]/g, '').replace(/(\..*?)\./g, '$1');
|
// const num = parseFloat(cleaned);
|
||||||
const num = parseFloat(cleaned);
|
// if (isNaN(num)) return;
|
||||||
if (isNaN(num)) return;
|
// e.target.value = num.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
e.target.value = num.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
// });
|
||||||
});
|
// }
|
||||||
}
|
|
||||||
document.querySelectorAll('input[name="Tax_Exempt"]').forEach(radio => {
|
document.querySelectorAll('input[name="Tax_Exempt"]').forEach(radio => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
checkRequiredFields();
|
checkRequiredFields();
|
||||||
@@ -747,23 +706,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce phone format (###) ###-####; reject otherwise
|
// FUTURE: Phone number format coercion
|
||||||
const phoneRaw = (data.Phone_Number || '').toString();
|
// When re-enabled, enforce strict format: (###) ###-#### or "Unknown" (or blank to allow)
|
||||||
const isUnknown = phoneRaw.toLowerCase() === 'unknown';
|
// Currently: Phone is optional, no validation on submit
|
||||||
if (!isUnknown) {
|
const phoneRaw = (data.Phone_Number || '').toString().trim();
|
||||||
const digits = phoneRaw.replace(/\D/g, '').slice(0, 10);
|
data.Phone_Number = phoneRaw; // Accept as-is if provided, or empty if blank
|
||||||
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
|
// Add user's PocketBase token
|
||||||
data.pbToken = pb.authStore.token;
|
data.pbToken = pb.authStore.token;
|
||||||
@@ -788,47 +735,10 @@
|
|||||||
data.Start_Date = formatDateToShort(data.Start_Date);
|
data.Start_Date = formatDateToShort(data.Start_Date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address validation: require plausible address, coordinates, OR Address_Notes
|
// Address and Job Name: optional, no validation
|
||||||
const addr = (data.Job_Address || '').toString().trim();
|
data.Job_Address = (data.Job_Address || '').toString().trim();
|
||||||
const addrNotes = (data.Address_Notes || '').toString().trim();
|
data.Address_Notes = (data.Address_Notes || '').toString().trim();
|
||||||
|
data.Job_Name = (data.Job_Name || '').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
|
// Convert Tax_Exempt to boolean
|
||||||
if (data.Tax_Exempt) {
|
if (data.Tax_Exempt) {
|
||||||
@@ -952,7 +862,7 @@
|
|||||||
|
|
||||||
<!-- Version Info -->
|
<!-- Version Info -->
|
||||||
<div class="fixed bottom-4 right-4 text-xs text-gray-400">
|
<div class="fixed bottom-4 right-4 text-xs text-gray-400">
|
||||||
v1.0.0-beta3
|
v1.0.0-beta3a
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ function calculateJobFullName(record: any): string {
|
|||||||
const companyClient = record.Company_Client || record.company_client || '';
|
const companyClient = record.Company_Client || record.company_client || '';
|
||||||
|
|
||||||
const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim());
|
const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim());
|
||||||
return parts.map(p => String(p).trim()).join(' - ').trim();
|
return parts.map(p => String(p).trim()).join(' ').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -518,7 +518,7 @@ async function addToExcel(record: any, pbId: string) {
|
|||||||
|
|
||||||
// Job_Full_Name: CONCAT(Job_Name, " - ", Job_Address, " - ", Company_Client)
|
// Job_Full_Name: CONCAT(Job_Name, " - ", Job_Address, " - ", Company_Client)
|
||||||
if (colLower === 'job_full_name') {
|
if (colLower === 'job_full_name') {
|
||||||
return calculateJobFullName(record);
|
return '=CONCATENATE([@[Job_Name]]," ",[@[Job_Address]]," ",[@[Company_Client]])';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due_Date_Counter: Status-aware due date counter (ASAP, Passed due, Due today, X days)
|
// Due_Date_Counter: Status-aware due date counter (ASAP, Passed due, Due today, X days)
|
||||||
@@ -693,43 +693,10 @@ app.post('/api/submit', async (c) => {
|
|||||||
if (dueIso) data.Due_Date = dueIso;
|
if (dueIso) data.Due_Date = dueIso;
|
||||||
if (startIso) data.Start_Date = startIso;
|
if (startIso) data.Start_Date = startIso;
|
||||||
|
|
||||||
// Address validation: require plausible address, coordinates, OR Address_Notes provided
|
// Address and Job_Name are optional; trim but do not validate
|
||||||
const addr = (data.Job_Address || '').toString().trim();
|
data.Job_Address = (data.Job_Address || '').toString().trim();
|
||||||
const addrNotes = (data.Address_Notes || '').toString().trim();
|
data.Address_Notes = (data.Address_Notes || '').toString().trim();
|
||||||
|
data.Job_Name = (data.Job_Name || '').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) {
|
|
||||||
return c.json({
|
|
||||||
success: false,
|
|
||||||
message: 'Provide a real address (e.g., 123 Main St, City, State), coordinates (e.g., 34.0522, -118.2437), or enter directions in Address Notes.'
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
return c.json({
|
|
||||||
success: false,
|
|
||||||
message: 'Job name must be at least 4 characters with letters (no symbols-only, no N/A).'
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
data.Job_Name = jobName;
|
|
||||||
|
|
||||||
// Enforce Job_Type and Job_Division selection
|
// Enforce Job_Type and Job_Division selection
|
||||||
const jobType = (data.Job_Type || '').toString().trim();
|
const jobType = (data.Job_Type || '').toString().trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user