// 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'); const { Document, Packer, Paragraph, HeadingLevel, TextRun, convertInchesToTwip } = require('docx'); /** * Creates a manager info document template * @param {string} jobNumber - Job number * @param {string} jobName - Job name * @param {Object} record - Full job record with all data * @returns {Promise} Word document buffer */ async function createManagerInfoDocument(jobNumber, jobName, record) { // Extract fields from record, with fallbacks const jobAddress = record.Job_Address || record.job_address || ''; const companyClient = record.Company_Client || record.company_client || ''; const jobType = record.Job_Type || record.job_type || ''; const jobDivision = record.Job_Division || record.job_division || ''; const contactPerson = record.Contact_Person || record.contact_person || ''; const phoneNumber = record.Phone_Number || record.phone_number || ''; const jobStatus = record.Job_Status || record.job_status || ''; const doc = new Document({ sections: [{ properties: {}, children: [ // Header: Job # and Name new Paragraph({ text: `Job # ${jobNumber}`, heading: HeadingLevel.HEADING_1, spacing: { after: 0 } }), new Paragraph({ text: `Name: ${jobName}`, heading: HeadingLevel.HEADING_1, spacing: { after: 200 } }), // Project Info section new Paragraph({ text: 'Project Info', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }), new Paragraph({ children: [ new TextRun({ text: 'Job Name: ' }), new TextRun({ text: jobName, underline: {} }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: 'Job Address: ' }), new TextRun({ text: jobAddress, underline: {} }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: 'Job Division: ' }), new TextRun({ text: jobDivision, underline: {} }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: 'Customer/Client Name: ' }), new TextRun({ text: companyClient, underline: {} }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: 'Job Type: ' }), new TextRun({ text: jobType, underline: {} }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: 'Job Status: ' }), new TextRun({ text: jobStatus, underline: {} }) ], spacing: { after: 200 } }), // Estimates Information section new Paragraph({ text: 'Estimates Information', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }), new Paragraph({ text: 'Scope: _________________________________', spacing: { after: 50 } }), new Paragraph({ text: 'Main Vendors: _________________________________', spacing: { after: 50 } }), new Paragraph({ text: 'Estimate Cost: _________________________________', spacing: { after: 200 } }), // Schedule section new Paragraph({ text: 'Schedule', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }), new Paragraph({ text: 'Expected Start Date: _________________________________', spacing: { after: 50 } }), new Paragraph({ text: 'Actual Start Date: _________________________________', spacing: { after: 50 } }), new Paragraph({ text: 'Estimated End Date: _________________________________', spacing: { after: 200 } }), // Main Contacts section new Paragraph({ text: 'Main Contacts', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }), new Paragraph({ text: 'Project Manager (Name / Phone / Email):', spacing: { after: 50 } }), new Paragraph({ text: `${contactPerson}${phoneNumber ? ' / ' + phoneNumber : ''}`, spacing: { after: 100 } }), new Paragraph({ text: 'Superintendent (Name / Phone / Email):', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 100 } }), new Paragraph({ text: 'Assistant/Coordinator (Name / Phone / Email):', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 100 } }), new Paragraph({ text: 'Billing Manager (Name / Phone / Email):', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 200 } }), // Job Notes & Key Info section new Paragraph({ text: 'Job Notes & Key Info', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }), new Paragraph({ text: '_________________________________________________________________', spacing: { after: 50 } }) ] }] }); return await Packer.toBuffer(doc); } /** * 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 * @param {Object} record - Full job record with all data * @returns {Promise} Result with folder IDs and share link */ async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName, record) { 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 and upload Manager Info document to Managers Info subfolder console.log('[Job Folder Integration] Creating and uploading Manager Info document'); try { const docBuffer = await createManagerInfoDocument(jobNumber, jobFullName, record); const uploadResp = await fetch( `https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${subFolderId}:/${jobNumber} - Manager Info.docx:/content`, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }, body: docBuffer } ); if (!uploadResp.ok) { const errorData = await uploadResp.text(); console.warn('[Job Folder Integration] Failed to upload Manager Info document:', { status: uploadResp.status, error: errorData }); } else { console.log('[Job Folder Integration] ✓ Manager Info document uploaded successfully'); } } catch (docError) { console.warn('[Job Folder Integration] Error creating/uploading Manager Info document:', docError); } // 4) 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, createManagerInfoDocument };