From 9c73773c19126b5185770001b11321fa42339d1d Mon Sep 17 00:00:00 2001 From: aewing Date: Sat, 20 Dec 2025 05:16:39 +0000 Subject: [PATCH] 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 --- extracted-graph-logic/bun.lock | 41 ++++ .../job-folder-integration.js | 220 +++++++++++++++++- extracted-graph-logic/package.json | 3 +- .../post-record-integration.js | 3 +- 4 files changed, 262 insertions(+), 5 deletions(-) diff --git a/extracted-graph-logic/bun.lock b/extracted-graph-logic/bun.lock index da86fe3..9135df6 100644 --- a/extracted-graph-logic/bun.lock +++ b/extracted-graph-logic/bun.lock @@ -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=="], } } diff --git a/extracted-graph-logic/job-folder-integration.js b/extracted-graph-logic/job-folder-integration.js index 4148233..db62532 100644 --- a/extracted-graph-logic/job-folder-integration.js +++ b/extracted-graph-logic/job-folder-integration.js @@ -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} 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} 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 }; diff --git a/extracted-graph-logic/package.json b/extracted-graph-logic/package.json index ce80dd9..ed1881a 100644 --- a/extracted-graph-logic/package.json +++ b/extracted-graph-logic/package.json @@ -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", diff --git a/extracted-graph-logic/post-record-integration.js b/extracted-graph-logic/post-record-integration.js index 46f6c17..f43c57a 100644 --- a/extracted-graph-logic/post-record-integration.js +++ b/extracted-graph-logic/post-record-integration.js @@ -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');