v1.0.0-beta3: Add Planner task creation with notes, fix folder name trimming, fix Excel folder link fallback

This commit is contained in:
2025-12-22 20:00:45 +00:00
parent 65fb9a52d9
commit 5d9abb4cae
3 changed files with 112 additions and 41 deletions
-26
View File
@@ -2,32 +2,6 @@
All notable changes to this project will be documented in this file. 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 ## v1.0.0-beta2 - 2025-12-19
- UI - UI
+1 -12
View File
@@ -536,15 +536,6 @@
const result = await response.json(); const result = await response.json();
if (result.success) { 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 = `<a href="${result.jobFolderLink}" target="_blank" rel="noopener" class="text-blue-600 underline">Open job folder</a>`;
linkContainer.classList.remove('hidden');
}
}
message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200'; message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200';
message.textContent = result.message; message.textContent = result.message;
message.classList.remove('hidden'); message.classList.remove('hidden');
@@ -565,10 +556,8 @@
</script> </script>
<!-- Version Info --> <!-- Version Info -->
<!-- Folder link container -->
<div id="folderLinkContainer" class="fixed bottom-16 right-4 text-sm text-blue-600 hidden"></div>
<div class="fixed bottom-4 right-4 text-xs text-gray-400"> <div class="fixed bottom-4 right-4 text-xs text-gray-400">
v1.0.0-beta2 v1.0.0-beta3
</div> </div>
</body> </body>
</html> </html>
+110 -2
View File
@@ -273,7 +273,7 @@ function calculateJobFullName(record: any): string {
const companyClient = record.Company_Client || record.company_client || ''; const companyClient = record.Company_Client || record.company_client || '';
const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim()); 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); 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 // Initialize table cache on startup
async function initializeTableCache() { async function initializeTableCache() {
const client = await getGraphClient(); const client = await getGraphClient();
@@ -542,8 +620,19 @@ async function updateExcelFolderLink(pbId: string, link: string) {
return vals && vals[pbIdColIndex] === pbId; return vals && vals[pbIdColIndex] === pbId;
}); });
if (!target) { if (!target) {
console.warn('⚠️ Excel row not found for pb_id:', pbId); // 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; 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) // 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 }); 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) // Send Teams notification (fire and forget, don't block response)
notifyTeamsChannel(record).catch(err => { notifyTeamsChannel(record).catch(err => {
console.error('⚠️ Teams notification error:', err.message); console.error('⚠️ Teams notification error:', err.message);