feat: implement dotenv secrets loading from /home/admin/secrets/.env
- Added dotenv package to load environment variables from absolute path - Secrets now loaded automatically on app startup, no manual sourcing needed - Secrets file secured with chmod 600, outside project directory - Enables persistent secrets across service restarts - Tested: Job submission flow working end-to-end with all steps completing
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
# Job Folder Integration Module
|
||||||
|
|
||||||
|
Automatically creates OneDrive folders with share links when PocketBase job records are created.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd extracted-graph-logic
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### 1. Import in server.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { processNewJobRecord } from './extracted-graph-logic/post-record-integration.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Call after PocketBase record creation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const graphToken = await getGraphToken();
|
||||||
|
const folderResult = await processNewJobRecord(record, graphToken);
|
||||||
|
|
||||||
|
if (folderResult.success) {
|
||||||
|
record.Job_Folder_Link = folderResult.shareLink || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
await addToExcel(record, record.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
1. Receives PocketBase record + Graph API token
|
||||||
|
2. Creates folder: `{Job_Number} - {Job_Name} - {Job_Address} - {Company_Client}`
|
||||||
|
3. Creates "Managers Info" subfolder
|
||||||
|
4. Generates organization-wide share link
|
||||||
|
5. Returns folder details and link
|
||||||
|
|
||||||
|
## Module Files
|
||||||
|
|
||||||
|
- **post-record-integration.js** - Main entry point, exports processNewJobRecord()
|
||||||
|
- **job-folder-integration.js** - Core Graph API logic, exports createJobFolderAndGetLink()
|
||||||
|
- **config.js** - Production configuration:
|
||||||
|
- DRIVE_ID: b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
|
||||||
|
- PARENT_ITEM_ID: 01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
|
||||||
|
- CLIENT_ID: 3c846e71-9609-40e1-b458-0eb805e21b9f
|
||||||
|
- **package.json** - Dependencies (node-fetch@2.7.0)
|
||||||
|
|
||||||
|
## Return Values
|
||||||
|
|
||||||
|
### On Success
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
jobNumber: "12345",
|
||||||
|
jobFullName: "New Construction - 123 Main St - ABC Corp",
|
||||||
|
shareLink: "https://...",
|
||||||
|
mainFolderId: "...",
|
||||||
|
mainFolderName: "...",
|
||||||
|
subFolderId: "...",
|
||||||
|
folderPath: "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### On Failure
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: "error message",
|
||||||
|
jobNumber: "12345",
|
||||||
|
shareLink: null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit config.js to change OneDrive location. Current values point to Cardoza Construction production SharePoint/OneDrive.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If folder creation fails, job is still created in PocketBase and synced to Excel
|
||||||
|
- Share link will be null on failure
|
||||||
|
- Errors logged with `[Job Folder Integration]` prefix
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- node-fetch@2.7.0 (for Graph API calls)
|
||||||
|
- Graph API token from your server
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Production-ready. Uses actual OneDrive/SharePoint IDs from Job Creation Form environment.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "job-folder-graph-integration",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// SharePoint/OneDrive Configuration
|
||||||
|
// These IDs specify where folders and files will be created
|
||||||
|
|
||||||
|
const DRIVE_ID = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||||
|
const PARENT_ITEM_ID = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
|
||||||
|
|
||||||
|
// Azure AD App Configuration
|
||||||
|
const CLIENT_ID = "3c846e71-9609-40e1-b458-0eb805e21b9f";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DRIVE_ID,
|
||||||
|
PARENT_ITEM_ID,
|
||||||
|
CLIENT_ID
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Job Folder Integration Module
|
||||||
|
// This module handles folder creation and share link capture after PocketBase record creation
|
||||||
|
// Uses the same Graph API token from the server to avoid re-authentication
|
||||||
|
|
||||||
|
const { DRIVE_ID, PARENT_ITEM_ID } = require('./config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a job folder structure in OneDrive/SharePoint and returns the share link
|
||||||
|
* @param {string} accessToken - Graph API access token (from server's token flow)
|
||||||
|
* @param {string} jobNumber - Job number (e.g., "12345")
|
||||||
|
* @param {string} jobFullName - Full job name calculated from form data
|
||||||
|
* @returns {Promise<Object>} Result with folder IDs and share link
|
||||||
|
*/
|
||||||
|
async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName) {
|
||||||
|
try {
|
||||||
|
// 1) Create main folder under configured parent
|
||||||
|
const mainFolderName = `${jobNumber} - ${jobFullName}`;
|
||||||
|
const createFolderEndpoint = `https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${PARENT_ITEM_ID}/children`;
|
||||||
|
|
||||||
|
console.log(`[Job Folder Integration] Creating main folder: ${mainFolderName}`);
|
||||||
|
const folderResp = await fetch(createFolderEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: mainFolderName,
|
||||||
|
folder: {},
|
||||||
|
"@microsoft.graph.conflictBehavior": "rename"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderData = await folderResp.json();
|
||||||
|
console.log(`[Job Folder Integration] Create folder response:`, {
|
||||||
|
status: folderResp.status,
|
||||||
|
folderId: folderData.id,
|
||||||
|
folderName: folderData.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResp.ok) {
|
||||||
|
throw new Error(`Create main folder failed: ${JSON.stringify(folderData)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainFolderId = folderData.id;
|
||||||
|
|
||||||
|
// 2) Create subfolder "Managers Info"
|
||||||
|
console.log('[Job Folder Integration] Creating subfolder: Managers Info');
|
||||||
|
const subFolderResp = await fetch(
|
||||||
|
`https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${mainFolderId}/children`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Managers Info',
|
||||||
|
folder: {},
|
||||||
|
"@microsoft.graph.conflictBehavior": "rename"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const subFolderData = await subFolderResp.json();
|
||||||
|
console.log('[Job Folder Integration] Create subfolder response:', {
|
||||||
|
status: subFolderResp.status,
|
||||||
|
subfolderId: subFolderData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subFolderResp.ok) {
|
||||||
|
throw new Error(`Create subfolder failed: ${JSON.stringify(subFolderData)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subFolderId = subFolderData.id;
|
||||||
|
|
||||||
|
// 3) Create share link for the main folder
|
||||||
|
console.log('[Job Folder Integration] Creating share link for main folder');
|
||||||
|
const shareResp = await fetch(
|
||||||
|
`https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${mainFolderId}/createLink`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'view',
|
||||||
|
scope: 'organization'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const shareData = await shareResp.json();
|
||||||
|
console.log('[Job Folder Integration] Share link response:', {
|
||||||
|
status: shareResp.status,
|
||||||
|
hasLink: !!shareData.link?.webUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shareResp.ok) {
|
||||||
|
throw new Error(`Create share link failed: ${JSON.stringify(shareData)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareLink = shareData.link && shareData.link.webUrl ? shareData.link.webUrl : null;
|
||||||
|
|
||||||
|
console.log(`[Job Folder Integration] ✓ Successfully created folder structure and share link`);
|
||||||
|
console.log(`[Job Folder Integration] Share link: ${shareLink}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
mainFolderId,
|
||||||
|
mainFolderName,
|
||||||
|
subFolderId,
|
||||||
|
shareLink,
|
||||||
|
folderPath: mainFolderName
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Job Folder Integration] Error in createJobFolderAndGetLink:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createJobFolderAndGetLink
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "job-folder-graph-integration",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Microsoft Graph integration for job folder creation and management",
|
||||||
|
"main": "post-record-integration.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node example-usage.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"microsoft-graph",
|
||||||
|
"onedrive",
|
||||||
|
"sharepoint",
|
||||||
|
"folder-management"
|
||||||
|
],
|
||||||
|
"author": "Cardoza Construction",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Post-Record Integration Module
|
||||||
|
// Handles all post-PocketBase record creation tasks:
|
||||||
|
// 1. Captures form data and Job_Number from the created PB record
|
||||||
|
// 2. Creates folder structure in OneDrive using the same Graph token
|
||||||
|
// 3. Captures share link from created folder
|
||||||
|
// 4. Returns the share link to be added to Excel row
|
||||||
|
|
||||||
|
const { createJobFolderAndGetLink } = require('./job-folder-integration');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a newly created PocketBase record by:
|
||||||
|
* - Creating corresponding folder structure in OneDrive
|
||||||
|
* - Capturing the share link for the folder
|
||||||
|
*
|
||||||
|
* @param {Object} record - The PocketBase record that was just created
|
||||||
|
* @param {string} accessToken - Graph API access token (from server)
|
||||||
|
* @returns {Promise<Object>} Result with share link and folder info
|
||||||
|
*/
|
||||||
|
async function processNewJobRecord(record, accessToken) {
|
||||||
|
try {
|
||||||
|
console.log('[Post-Record Integration] Processing new job record:', {
|
||||||
|
jobNumber: record.Job_Number,
|
||||||
|
jobName: record.Job_Name,
|
||||||
|
recordId: record.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract required fields from the record
|
||||||
|
const jobNumber = record.Job_Number || record.job_number;
|
||||||
|
const jobName = record.Job_Name || record.job_name || '';
|
||||||
|
const jobAddress = record.Job_Address || record.job_address || '';
|
||||||
|
const companyClient = record.Company_Client || record.company_client || '';
|
||||||
|
|
||||||
|
// Calculate Job_Full_Name (same logic as in server.ts)
|
||||||
|
const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim());
|
||||||
|
const jobFullName = parts.join(' - ');
|
||||||
|
|
||||||
|
console.log('[Post-Record Integration] Calculated job full name:', jobFullName);
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!jobNumber) {
|
||||||
|
throw new Error('Job_Number is required but was not found in record');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jobFullName || jobFullName.trim() === '') {
|
||||||
|
throw new Error('Job_Full_Name could not be calculated from record data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create folder and get share link
|
||||||
|
const folderResult = await createJobFolderAndGetLink(
|
||||||
|
accessToken,
|
||||||
|
jobNumber,
|
||||||
|
jobFullName
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[Post-Record Integration] ✓ Folder creation successful');
|
||||||
|
console.log('[Post-Record Integration] Share link:', folderResult.shareLink);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
jobNumber,
|
||||||
|
jobFullName,
|
||||||
|
shareLink: folderResult.shareLink,
|
||||||
|
mainFolderId: folderResult.mainFolderId,
|
||||||
|
mainFolderName: folderResult.mainFolderName,
|
||||||
|
subFolderId: folderResult.subFolderId,
|
||||||
|
folderPath: folderResult.folderPath
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Post-Record Integration] Error processing new job record:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
jobNumber: record.Job_Number || record.job_number,
|
||||||
|
shareLink: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
processNewJobRecord
|
||||||
|
};
|
||||||
@@ -536,6 +536,15 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
// Log and display the folder link when available
|
||||||
|
if (result.jobFolderLink) {
|
||||||
|
console.log(`📁 Folder link: ${result.jobFolderLink}`);
|
||||||
|
const linkContainer = document.getElementById('folderLinkContainer');
|
||||||
|
if (linkContainer) {
|
||||||
|
linkContainer.innerHTML = `<a href="${result.jobFolderLink}" target="_blank" rel="noopener" class="text-blue-600 underline">Open job folder</a>`;
|
||||||
|
linkContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200';
|
message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200';
|
||||||
message.textContent = result.message;
|
message.textContent = result.message;
|
||||||
message.classList.remove('hidden');
|
message.classList.remove('hidden');
|
||||||
@@ -556,6 +565,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Version Info -->
|
<!-- Version Info -->
|
||||||
|
<!-- Folder link container -->
|
||||||
|
<div id="folderLinkContainer" class="fixed bottom-16 right-4 text-sm text-blue-600 hidden"></div>
|
||||||
<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-beta2
|
v1.0.0-beta2
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { serveStatic } from 'hono/bun';
|
import { serveStatic } from 'hono/bun';
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
import { Client } from '@microsoft/microsoft-graph-client';
|
import { Client } from '@microsoft/microsoft-graph-client';
|
||||||
import { config } from 'dotenv';
|
|
||||||
import { mkdir, appendFile } from 'fs/promises';
|
import { mkdir, appendFile } from 'fs/promises';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { processNewJobRecord } from './extracted-graph-logic/post-record-integration.js';
|
||||||
|
|
||||||
config();
|
// Load secrets from absolute path (not in project directory)
|
||||||
|
config({ path: '/home/admin/secrets/.env' });
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ let tableCache: {
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
// Resolve the Excel table name from env; prefers EXCEL_TABLE_NAME (just the table's name),
|
// 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 Job_List.
|
// and falls back to parsing EXCEL_TABLE if provided as a full path, else default to Test_Table.
|
||||||
function resolveTableName(): string {
|
function resolveTableName(): string {
|
||||||
const nameFromSimpleEnv = process.env.EXCEL_TABLE_NAME?.trim();
|
const nameFromSimpleEnv = process.env.EXCEL_TABLE_NAME?.trim();
|
||||||
if (nameFromSimpleEnv) return nameFromSimpleEnv;
|
if (nameFromSimpleEnv) return nameFromSimpleEnv;
|
||||||
@@ -132,7 +134,7 @@ function resolveTableName(): string {
|
|||||||
const nameFromPath = process.env.EXCEL_TABLE?.split('/tables/').pop()?.trim();
|
const nameFromPath = process.env.EXCEL_TABLE?.split('/tables/').pop()?.trim();
|
||||||
if (nameFromPath) return nameFromPath;
|
if (nameFromPath) return nameFromPath;
|
||||||
|
|
||||||
return 'Job_List';
|
return 'Test_Table';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -393,6 +395,48 @@ async function addToExcel(record: any, pbId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the Excel row's Job_Folder_Link for the given pb_id
|
||||||
|
async function updateExcelFolderLink(pbId: string, link: string) {
|
||||||
|
const client = await getGraphClient();
|
||||||
|
const tableName = resolveTableName();
|
||||||
|
const excelTablePath = `/drives/${process.env.DRIVE_ID}/items/${process.env.ITEM_ID}/workbook/tables/${tableName}`;
|
||||||
|
|
||||||
|
// Initialize cache if not already done
|
||||||
|
if (!tableCache) {
|
||||||
|
await initializeTableCache();
|
||||||
|
}
|
||||||
|
const { columnNames } = tableCache!;
|
||||||
|
const lower = columnNames.map((n) => n.toLowerCase());
|
||||||
|
const linkColIndex = lower.indexOf('job_folder_link');
|
||||||
|
const pbIdColIndex = lower.indexOf('pb_id');
|
||||||
|
if (linkColIndex === -1 || pbIdColIndex === -1) {
|
||||||
|
console.warn('⚠️ Excel table missing Job_Folder_Link or pb_id column');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsResp = await client.api(`${excelTablePath}/rows`).get();
|
||||||
|
const rows = rowsResp?.value || [];
|
||||||
|
const target = rows.find((row: any) => {
|
||||||
|
const vals = row?.values?.[0];
|
||||||
|
return vals && vals[pbIdColIndex] === pbId;
|
||||||
|
});
|
||||||
|
if (!target) {
|
||||||
|
console.warn('⚠️ Excel row not found for pb_id:', pbId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update using table row itemAt - update the full row with link populated
|
||||||
|
const currentValues = [...target.values[0]]; // Copy array
|
||||||
|
currentValues[linkColIndex] = link || '';
|
||||||
|
|
||||||
|
await client.api(`${excelTablePath}/rows/itemAt(index=${target.index})`)
|
||||||
|
.patch({
|
||||||
|
values: [currentValues]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ Updated Excel Job_Folder_Link for pb_id:', pbId);
|
||||||
|
}
|
||||||
|
|
||||||
// API endpoint to submit job
|
// API endpoint to submit job
|
||||||
app.post('/api/submit', async (c) => {
|
app.post('/api/submit', async (c) => {
|
||||||
let data: any;
|
let data: any;
|
||||||
@@ -465,9 +509,35 @@ app.post('/api/submit', async (c) => {
|
|||||||
const record = await pb.collection(process.env.PB_COLLECTION!).create(data);
|
const record = await pb.collection(process.env.PB_COLLECTION!).create(data);
|
||||||
console.log(`✓ Created PocketBase record with Job_Number: ${newJobNumber}, ID: ${record.id}`);
|
console.log(`✓ Created PocketBase record with Job_Number: ${newJobNumber}, ID: ${record.id}`);
|
||||||
|
|
||||||
// Add to Excel with pb_id = record.id
|
// Persist calculated fields to PocketBase immediately after creation
|
||||||
|
try {
|
||||||
|
const fullName = calculateJobFullName(record);
|
||||||
|
const qbLink = calculateJobQbLink(record);
|
||||||
|
const voxerLinkVal = calculateVoxerLink(record);
|
||||||
|
const jobCodesVal = calculateJobCodes(record);
|
||||||
|
|
||||||
|
await pb.collection(process.env.PB_COLLECTION!).update(record.id, {
|
||||||
|
Job_Full_Name: fullName,
|
||||||
|
Job_QB_Link: qbLink,
|
||||||
|
Voxer_Link: voxerLinkVal,
|
||||||
|
Job_Codes: jobCodesVal
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep local record in sync for Excel add
|
||||||
|
(record as any).Job_Full_Name = fullName;
|
||||||
|
(record as any).Job_QB_Link = qbLink;
|
||||||
|
(record as any).Voxer_Link = voxerLinkVal;
|
||||||
|
(record as any).Job_Codes = jobCodesVal;
|
||||||
|
console.log('✓ Updated PB with calculated fields (Full Name, QB, Voxer, Codes)');
|
||||||
|
} catch (pbUpdateErr: any) {
|
||||||
|
console.error('⚠️ Failed to update PB calculated fields:', pbUpdateErr);
|
||||||
|
await logError('PocketBase Calculated Fields Update', pbUpdateErr, { recordId: record.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to Excel first with pb_id = record.id (dynamic formulas for Due_Date_Counter, Active)
|
||||||
try {
|
try {
|
||||||
await addToExcel(record, record.id);
|
await addToExcel(record, record.id);
|
||||||
|
console.log('✓ Excel row created');
|
||||||
} catch (excelError: any) {
|
} catch (excelError: any) {
|
||||||
console.error('⚠️ Excel sync failed, but PocketBase record was created:', excelError);
|
console.error('⚠️ Excel sync failed, but PocketBase record was created:', excelError);
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -476,15 +546,66 @@ app.post('/api/submit', async (c) => {
|
|||||||
message: `Job created successfully with Job Number: ${newJobNumber}`,
|
message: `Job created successfully with Job Number: ${newJobNumber}`,
|
||||||
jobId: record.id,
|
jobId: record.id,
|
||||||
jobNumber: newJobNumber,
|
jobNumber: newJobNumber,
|
||||||
|
jobFolderLink: null,
|
||||||
excelError: excelError.message
|
excelError: excelError.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After Excel is added, create folder structure and capture share link
|
||||||
|
let jobFolderLink = '';
|
||||||
|
try {
|
||||||
|
const graphToken = await getGraphToken();
|
||||||
|
const folderResult = await processNewJobRecord(record, graphToken) as any;
|
||||||
|
|
||||||
|
if (folderResult.success) {
|
||||||
|
jobFolderLink = folderResult.shareLink || '';
|
||||||
|
console.log(`✓ Folder created and share link captured: ${jobFolderLink}`);
|
||||||
|
console.log(`📁 Open folder: ${jobFolderLink}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Folder creation failed: ${folderResult.error}`);
|
||||||
|
}
|
||||||
|
} catch (folderError: any) {
|
||||||
|
console.error('⚠️ Folder creation failed, continuing without folder link:', folderError);
|
||||||
|
await logError('Folder Creation', folderError, {
|
||||||
|
jobNumber: newJobNumber,
|
||||||
|
recordId: record.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Excel row with Job_Folder_Link (if available)
|
||||||
|
if (jobFolderLink) {
|
||||||
|
try {
|
||||||
|
await updateExcelFolderLink(record.id, jobFolderLink);
|
||||||
|
} catch (excelUpdateErr: any) {
|
||||||
|
console.error('⚠️ Failed to update Excel with folder link:', excelUpdateErr);
|
||||||
|
await logError('Excel Update Folder Link', excelUpdateErr, { recordId: record.id, jobFolderLink });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist static values back to PocketBase: Job_Folder_Link, Due_Date_Counter, Active
|
||||||
|
try {
|
||||||
|
const dueStatic = calculateDueDateCounter(record);
|
||||||
|
const activeStatic = calculateActive(record);
|
||||||
|
await pb.collection(process.env.PB_COLLECTION!).update(record.id, {
|
||||||
|
Job_Folder_Link: jobFolderLink || '',
|
||||||
|
Due_Date_Counter: dueStatic,
|
||||||
|
Active: activeStatic
|
||||||
|
});
|
||||||
|
(record as any).Job_Folder_Link = jobFolderLink || '';
|
||||||
|
(record as any).Due_Date_Counter = dueStatic;
|
||||||
|
(record as any).Active = activeStatic;
|
||||||
|
console.log('✓ Updated PB with folder link, due date counter (static), and active (boolean)');
|
||||||
|
} catch (pbFinalizeErr: any) {
|
||||||
|
console.error('⚠️ Failed to update PB with final static values:', pbFinalizeErr);
|
||||||
|
await logError('PocketBase Final Update', pbFinalizeErr, { recordId: record.id, jobFolderLink });
|
||||||
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Job created successfully with Job Number: ${newJobNumber} and synced to Excel`,
|
message: `Job created successfully with Job Number: ${newJobNumber} and synced to Excel`,
|
||||||
jobId: record.id,
|
jobId: record.id,
|
||||||
jobNumber: newJobNumber
|
jobNumber: newJobNumber,
|
||||||
|
jobFolderLink: jobFolderLink || null
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('✗ Error submitting job:', error);
|
console.error('✗ Error submitting job:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user