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:
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user