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
This commit is contained in:
2025-12-20 05:16:39 +00:00
parent 340b7e0818
commit 9c73773c19
4 changed files with 262 additions and 5 deletions
+41
View File
@@ -5,17 +5,58 @@
"": {
"name": "job-folder-graph-integration",
"dependencies": {
"docx": "^8.5.0",
"node-fetch": "^2.7.0",
},
},
},
"packages": {
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"docx": ["docx@8.6.0", "", { "dependencies": { "@types/node": "^20.3.1", "jszip": "^3.10.1", "nanoid": "^5.0.4", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-JEzPozEsuGIyUkEqdGlCv/b1avYeXjR4PjwiLdPwRKdsI9spBCq4WP0QcGYfIANpgYdJSphw4IT8M/a9dpnpvQ=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"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=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"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=="],
"xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="],
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
}
}
+217 -3
View File
@@ -3,15 +3,199 @@
// 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) {
async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName, record) {
try {
// 1) Create main folder under configured parent
const mainFolderName = `${jobNumber} - ${jobFullName}`;
@@ -74,7 +258,36 @@ async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName) {
const subFolderId = subFolderData.id;
// 3) Create share link for the main folder
// 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`,
@@ -122,5 +335,6 @@ async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName) {
}
module.exports = {
createJobFolderAndGetLink
createJobFolderAndGetLink,
createManagerInfoDocument
};
+2 -1
View File
@@ -7,7 +7,8 @@
"test": "node example-usage.js"
},
"dependencies": {
"node-fetch": "^2.7.0"
"node-fetch": "^2.7.0",
"docx": "^8.5.0"
},
"keywords": [
"microsoft-graph",
@@ -49,7 +49,8 @@ async function processNewJobRecord(record, accessToken) {
const folderResult = await createJobFolderAndGetLink(
accessToken,
jobNumber,
jobFullName
jobFullName,
record
);
console.log('[Post-Record Integration] ✓ Folder creation successful');