Files
Job-Form/extracted-graph-logic/job-folder-integration.js
T
aewing 9c73773c19 feat: add Manager Info document generation in Managers Info folder
- Create Word document template with job information pre-populated
- Auto-populate: Job Name, Address, Division, Client, Type, Status, Contact info
- Include fillable fields: Scope, Vendors, Estimate Cost, Dates, Team Contacts, Notes
- Document named [JobNumber] - Manager Info.docx for easy identification
- Uses docx library to generate professional Word documents
- Document uploaded automatically during job folder creation
2025-12-20 05:16:39 +00:00

341 lines
11 KiB
JavaScript

// 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, 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<Buffer>} 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({
text: `Job Name: ${jobName}`,
spacing: { after: 50 }
}),
new Paragraph({
text: `Job Address: ${jobAddress}`,
spacing: { after: 50 }
}),
new Paragraph({
text: `Job Division: ${jobDivision}`,
spacing: { after: 50 }
}),
new Paragraph({
text: `Customer/Client Name: ${companyClient}`,
spacing: { after: 50 }
}),
new Paragraph({
text: `Job Type: ${jobType}`,
spacing: { after: 50 }
}),
new Paragraph({
text: `Job Status: ${jobStatus}`,
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<Object>} 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
};