diff --git a/frontend/index.html b/frontend/index.html index d93a1f3..5202d10 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,8 @@ Job Creation Form + + diff --git a/server.ts b/server.ts index 87fd8a2..1cf1b1b 100644 --- a/server.ts +++ b/server.ts @@ -693,6 +693,54 @@ app.post('/api/submit', async (c) => { if (dueIso) data.Due_Date = dueIso; if (startIso) data.Start_Date = startIso; + // Address validation: require plausible address, coordinates, OR Address_Notes provided + 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) { + 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 + const jobType = (data.Job_Type || '').toString().trim(); + const jobDivision = (data.Job_Division || '').toString().trim(); + if (!jobType || !jobDivision) { + return c.json({ + success: false, + message: 'Please select both Job Type and Job Division.' + }, 400); + } + // Get all existing job numbers and find the max const existingJobs = await pb.collection(process.env.PB_COLLECTION!).getFullList({ fields: 'Job_Number',