From 5d9abb4caeda2ed19fc5b4023623322c6464f4e0 Mon Sep 17 00:00:00 2001 From: aewing Date: Mon, 22 Dec 2025 20:00:45 +0000 Subject: [PATCH] v1.0.0-beta3: Add Planner task creation with notes, fix folder name trimming, fix Excel folder link fallback --- CHANGELOG.md | 26 ---------- frontend/index.html | 13 +---- server.ts | 114 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4710eeb..872cc0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,32 +2,6 @@ All notable changes to this project will be documented in this file. -## v1.0.0-beta4 - 2025-12-20 - -- Teams Message Management - - Added UI (`frontend/teams-messages.html`) to load and review Teams channel messages with job details. - - Features: Auto-load via URL params, auto-select up to 50 messages, preview job card details, collapsible JSON. - - **Known Limitation**: Message deletion is not supported by Microsoft Teams Graph API for public channels (HTTP 405). - - This is a platform limitation, not an application error. - - Workaround 1: Delete messages directly from Teams client (right-click → Delete). - - Workaround 2: Use delegated user token with `ChatMessage.ReadWrite.All` permission (requires Azure AD configuration). - - Updated error handling to clearly explain Teams API limitations to users. -- Ops - - Enhanced error logging in server delete endpoint to capture full error details and distinguish between API errors and Teams platform limitations. - -## v1.0.0-beta3 - 2025-12-20 - -- Teams Notifications - - Fixed webhook 400 error by adding `contentUrl: null` to Adaptive Card attachment payload. - - Reformatted card from FactSet to TextBlocks with markdown bold labels (`**Label:** value`) and `spacing: 'None'` for compact rows. - - Added timestamp to notification card. - - Removed folder button/actions from card - notifications now show job info only. - - Removed unused `buildMessageCard` function and simplified `buildAdaptiveCard`/`notifyTeamsChannel` signatures. -- Excel Sync - - Fixed Active formula being overwritten: `updateExcelFolderLink` now uses Graph API `cell(row,column)` method to update only the Job_Folder_Link cell, preserving formulas in other columns. -- Versioning - - Bumped `package.json` to `1.0.0-beta3`. - ## v1.0.0-beta2 - 2025-12-19 - UI diff --git a/frontend/index.html b/frontend/index.html index 4f541a9..d93a1f3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -536,15 +536,6 @@ const result = await response.json(); if (result.success) { - // Log and display the folder link when available - if (result.jobFolderLink) { - console.log(`📁 Folder link: ${result.jobFolderLink}`); - const linkContainer = document.getElementById('folderLinkContainer'); - if (linkContainer) { - linkContainer.innerHTML = `Open job folder`; - linkContainer.classList.remove('hidden'); - } - } message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200'; message.textContent = result.message; message.classList.remove('hidden'); @@ -565,10 +556,8 @@ - -
- v1.0.0-beta2 + v1.0.0-beta3
diff --git a/server.ts b/server.ts index a14c244..f116acf 100644 --- a/server.ts +++ b/server.ts @@ -273,7 +273,7 @@ function calculateJobFullName(record: any): string { const companyClient = record.Company_Client || record.company_client || ''; const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim()); - return parts.join(' - '); + return parts.map(p => String(p).trim()).join(' - ').trim(); } /** @@ -376,6 +376,84 @@ function calculateActive(record: any): boolean { return activeStatuses.includes(jobStatus); } +// ============================================================================ +// PLANNER TASK CREATION +// ============================================================================ + +async function createPlannerTask({ + jobNumber, + jobFullName, + dueDate, +}: { + jobNumber: string; + jobFullName: string; + dueDate?: string; +}) { + try { + const client = await getGraphClient(); + + // Hardcoded assignee for testing: aewing@cardoza.construction + const assigneeId = '1a6e9d10-138a-43e4-a09e-1f50463148c9'; + const assigneeName = 'Alex Ewing'; // Display name + + // Parse due date if provided (ISO format to Date object) + let dueDateObj: any = undefined; + if (dueDate) { + const d = new Date(dueDate); + if (!isNaN(d.getTime())) { + dueDateObj = d.toISOString(); + } + } + + // Task title: "Disposition New Job Folder for [Job_Number] [Job_Full_Name]" + const title = `Disposition New Job Folder for ${jobNumber} ${jobFullName}`; + + // Create the Planner task + const groupId = process.env.PLANNER_GROUP_ID; + const planId = process.env.PLANNER_PLAN_ID; + const bucketId = process.env.PLANNER_BUCKET_ID; + + if (!groupId || !planId || !bucketId) { + throw new Error('Missing Planner configuration: PLANNER_GROUP_ID, PLANNER_PLAN_ID, or PLANNER_BUCKET_ID'); + } + + const newTask = await client.api('/planner/tasks').post({ + planId, + bucketId, + title, + assignments: { + [assigneeId]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }, + ...(dueDateObj ? { dueDateTime: dueDateObj } : {}), + }); + + // Set the Notes/Description via task details (match the working test script) + const taskDetails = await client.api(`/planner/tasks/${newTask.id}/details`).get(); + const detailsEtag = taskDetails['@odata.etag']; + console.log(`[Planner] Task created ${newTask.id}, fetched details ETag: ${detailsEtag}`); + await client + .api(`/planner/tasks/${newTask.id}/details`) + .header('If-Match', detailsEtag) + .patch({ + description: 'Move the new folder to the correct Job Pages folder.', + }); + console.log(`[Planner] Successfully patched description`); + + console.log(`✓ Planner task created: "${title}" assigned to ${assigneeName}`); + return { success: true, taskId: newTask.id, title }; + } catch (error: any) { + console.error('✗ Planner task creation failed:', error.message); + await logError('Planner Task Creation', error, { + jobNumber, + jobFullName, + }); + throw error; + } +} + // Initialize table cache on startup async function initializeTableCache() { const client = await getGraphClient(); @@ -542,8 +620,19 @@ async function updateExcelFolderLink(pbId: string, link: string) { return vals && vals[pbIdColIndex] === pbId; }); if (!target) { - console.warn('⚠️ Excel row not found for pb_id:', pbId); - return; + // Row was just inserted at index 0, try updating that directly + console.warn('⚠️ Excel row not found by pb_id, trying index 0 (just inserted)'); + try { + await client.api(`${excelTablePath}/rows/itemAt(index=0)/range/cell(row=0,column=${linkColIndex})`) + .patch({ + values: [[link || '']] + }); + console.log('✓ Updated Excel Job_Folder_Link cell at index 0 for pb_id:', pbId); + return; + } catch (fallbackErr: any) { + console.error('⚠️ Fallback update at index 0 also failed:', fallbackErr.message); + return; + } } // Update only the Job_Folder_Link cell to preserve formulas in other columns (e.g., Active) @@ -720,6 +809,25 @@ app.post('/api/submit', async (c) => { await logError('PocketBase Final Update', pbFinalizeErr, { recordId: record.id, jobFolderLink }); } + // Create Planner task for the new job (assigned to aewing@cardoza.construction) + try { + const jobFullName = (record as any).Job_Full_Name || ''; + + await createPlannerTask({ + jobNumber: newJobNumber, + jobFullName, + dueDate: data.Due_Date || undefined, + }); + console.log('✓ Planner task created successfully'); + } catch (plannerError: any) { + console.error('⚠️ Planner task creation failed, continuing:', plannerError.message); + await logError('Planner Task Creation', plannerError, { + jobNumber: newJobNumber, + recordId: record.id, + }); + // Don't fail the response; Planner is optional + } + // Send Teams notification (fire and forget, don't block response) notifyTeamsChannel(record).catch(err => { console.error('⚠️ Teams notification error:', err.message);