v1.0.0-beta3: Add Planner task creation with notes, fix folder name trimming, fix Excel folder link fallback
This commit is contained in:
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
return;
|
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)
|
// 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user