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.
|
||||
|
||||
## 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
|
||||
|
||||
+1
-12
@@ -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 = `<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.textContent = result.message;
|
||||
message.classList.remove('hidden');
|
||||
@@ -565,10 +556,8 @@
|
||||
</script>
|
||||
|
||||
<!-- 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">
|
||||
v1.0.0-beta2
|
||||
v1.0.0-beta3
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user