diff --git a/frontend/index.html b/frontend/index.html index 5202d10..81e3e83 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -64,7 +64,7 @@ -

Use a descriptive name (no symbols-only, no N/A).

@@ -96,7 +96,7 @@
@@ -203,7 +203,7 @@
-
@@ -247,8 +247,8 @@ @@ -260,9 +260,9 @@ +

Estimated date work will start.

@@ -290,10 +290,10 @@ +

Date estimate is due to client.

- @@ -354,7 +354,7 @@
@@ -392,12 +392,12 @@
@@ -407,13 +407,13 @@
-

Enter a rough value (e.g., $25,000.00). Will auto-format in USD.

-
@@ -592,14 +592,16 @@ 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)) { + 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).'; + 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 = ''; @@ -813,16 +815,18 @@ 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" + // 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) { + 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).'; + 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'; @@ -836,7 +840,7 @@ } try { - const response = await fetch('/api/submit', { + const response = await fetch('http://localhost:3020/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/inspect-tables.ts b/inspect-tables.ts new file mode 100644 index 0000000..03d2784 --- /dev/null +++ b/inspect-tables.ts @@ -0,0 +1,53 @@ + +import { config } from 'dotenv'; +import { Client } from '@microsoft/microsoft-graph-client'; + +config({ path: '/home/admin/secrets/.env' }); + +async function getGraphToken() { + const tokenEndpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`; + const params = new URLSearchParams(); + params.append('client_id', process.env.CLIENT_ID!); + params.append('client_secret', process.env.CLIENT_SECRET!); + params.append('scope', 'https://graph.microsoft.com/.default'); + params.append('grant_type', 'client_credentials'); + + const response = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params, + }); + + const data = await response.json(); + return data.access_token; +} + +async function inspectTables() { + const token = await getGraphToken(); + const client = Client.init({ + authProvider: (done) => { + done(null, token); + }, + }); + + const driveId = process.env.DRIVE_ID; + const itemId = process.env.ITEM_ID; + const baseUrl = `/drives/${driveId}/items/${itemId}/workbook/tables`; + + console.log(`Inspecting tables in file: ${itemId}`); + + try { + const res = await client.api(baseUrl).get(); + const tables = res.value; + + for (const table of tables) { + console.log(`\nTable: ${table.name}`); + const cols = await client.api(`${baseUrl}/${table.name}/columns`).get(); + console.log(`- Column count: ${cols.value.length}`); + } + } catch (err) { + console.error('Error:', err.message); + } +} + +inspectTables(); diff --git a/server.ts b/server.ts index 1cf1b1b..043f4b3 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,5 @@ import { config } from 'dotenv'; + import { Hono } from 'hono'; import { serveStatic } from 'hono/bun'; import { cors } from 'hono/cors'; @@ -13,6 +14,31 @@ import { processNewJobRecord } from './extracted-graph-logic/post-record-integra // Load secrets from absolute path (not in project directory) config({ path: '/home/admin/secrets/.env' }); +// ============================================================================ +// ENVIRONMENT SETTINGS (Toggle between TEST and PROD here) +// ============================================================================ +const MODE = 'TEST'; // CHANGE THIS TO 'PROD' TO GO LIVE + +const CONFIG = { + TEST: { + pbCollection: 'Job_Info_TestEnv', + excelTable: 'Test_Table', + description: '๐Ÿงช TEST ENVIRONMENT' + }, + PROD: { + pbCollection: process.env.PB_COLLECTION || 'Job_Info_Prod', + excelTable: process.env.EXCEL_TABLE_NAME || 'Job_List', + description: '๐Ÿš€ PRODUCTION ENVIRONMENT' + } +}; + +const activeConfig = MODE === 'PROD' ? CONFIG.PROD : CONFIG.TEST; +console.log('\n' + '*'.repeat(60)); +console.log(' RUNNING IN: ' + activeConfig.description); +console.log(' PocketBase: ' + activeConfig.pbCollection); +console.log(' Excel Table: ' + activeConfig.excelTable); +console.log('*'.repeat(60) + '\n'); + const app = new Hono(); // ============================================================================ @@ -249,13 +275,7 @@ let tableCache: { // Resolve the Excel table name from env; prefers EXCEL_TABLE_NAME (just the table's name), // and falls back to parsing EXCEL_TABLE if provided as a full path, else default to Test_Table. function resolveTableName(): string { - const nameFromSimpleEnv = process.env.EXCEL_TABLE_NAME?.trim(); - if (nameFromSimpleEnv) return nameFromSimpleEnv; - - const nameFromPath = process.env.EXCEL_TABLE?.split('/tables/').pop()?.trim(); - if (nameFromPath) return nameFromPath; - - return 'Test_Table'; + return activeConfig.excelTable; } // ============================================================================ @@ -648,6 +668,7 @@ async function updateExcelFolderLink(pbId: string, link: string) { // API endpoint to submit job app.post('/api/submit', async (c) => { + const pbCollection = activeConfig.pbCollection; let data: any; try { data = await c.req.json(); @@ -742,7 +763,7 @@ app.post('/api/submit', async (c) => { } // Get all existing job numbers and find the max - const existingJobs = await pb.collection(process.env.PB_COLLECTION!).getFullList({ + const existingJobs = await pb.collection(pbCollection).getFullList({ fields: 'Job_Number', sort: '-created', }); @@ -763,7 +784,7 @@ app.post('/api/submit', async (c) => { delete data.pbToken; // Create the record in PocketBase as the authenticated user - const record = await pb.collection(process.env.PB_COLLECTION!).create(data); + const record = await pb.collection(pbCollection).create(data); console.log(`โœ“ Created PocketBase record with Job_Number: ${newJobNumber}, ID: ${record.id}`); // Persist calculated fields to PocketBase immediately after creation @@ -773,7 +794,7 @@ app.post('/api/submit', async (c) => { const voxerLinkVal = calculateVoxerLink(record); const jobCodesVal = calculateJobCodes(record); - await pb.collection(process.env.PB_COLLECTION!).update(record.id, { + await pb.collection(pbCollection).update(record.id, { Job_Full_Name: fullName, Job_QB_Link: qbLink, Voxer_Link: voxerLinkVal, @@ -843,7 +864,7 @@ app.post('/api/submit', async (c) => { try { const dueStatic = calculateDueDateCounter(record); const activeStatic = calculateActive(record); - await pb.collection(process.env.PB_COLLECTION!).update(record.id, { + await pb.collection(pbCollection).update(record.id, { Job_Folder_Link: jobFolderLink || '', Due_Date_Counter: dueStatic, Active: activeStatic