@@ -392,12 +392,12 @@
Don't forget to file the paperwork!
@@ -407,13 +407,13 @@
@@ -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