Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4628eb9a1 | |||
| 6f02194c23 | |||
| cc52c11bf6 |
@@ -0,0 +1,42 @@
|
|||||||
|
# Session Notes
|
||||||
|
|
||||||
|
## 2026-01-07
|
||||||
|
|
||||||
|
### Context
|
||||||
|
- Picked up the Job-Form project
|
||||||
|
- Stack: TypeScript server, HTML frontend with TailwindCSS
|
||||||
|
- Current version: v1.0.0-beta4
|
||||||
|
|
||||||
|
### Project Summary
|
||||||
|
- **Purpose**: Job creation form with Excel sync and PocketBase backend
|
||||||
|
- **Features**:
|
||||||
|
- Form submission → PocketBase record → Excel sync
|
||||||
|
- Teams channel notifications (Adaptive Cards)
|
||||||
|
- Teams message management UI
|
||||||
|
|
||||||
|
### Known Issues / Limitations
|
||||||
|
- Teams message deletion not supported via Graph API (HTTP 405) — platform limitation
|
||||||
|
- Workaround: Delete messages directly in Teams client
|
||||||
|
|
||||||
|
### Today's Work
|
||||||
|
- [x] Migrate to full Bun-native stack (removed dotenv, use Bun file APIs)
|
||||||
|
- [x] Fix Planner task creation to use form's due date with proper UTC handling
|
||||||
|
- [x] Remove hardcoded test values — now uses activeConfig for all environments
|
||||||
|
- [x] Make assignee configurable via `PLANNER_ASSIGNEE_ID` env var
|
||||||
|
|
||||||
|
### PROD Mode Checklist
|
||||||
|
Before switching `MODE = 'PROD'`, ensure `/home/admin/secrets/.env` contains:
|
||||||
|
- `PB_COLLECTION` - PocketBase collection name (e.g., `Job_Info_Prod`)
|
||||||
|
- `EXCEL_TABLE_NAME` - Excel table name (e.g., `Job_List`)
|
||||||
|
- `PLANNER_GROUP_ID` - Microsoft Planner group ID
|
||||||
|
- `PLANNER_PLAN_ID` - Microsoft Planner plan ID
|
||||||
|
- `PLANNER_BUCKET_ID` - Microsoft Planner bucket ID
|
||||||
|
- `PLANNER_ASSIGNEE_ID` - User ID for Planner task assignee (e.g., `1a6e9d10-138a-43e4-a09e-1f50463148c9`)
|
||||||
|
- `PLANNER_ASSIGNEE_NAME` - Display name for assignee (e.g., `Alex Ewing`)
|
||||||
|
- All standard Azure AD + PocketBase creds (CLIENT_ID, CLIENT_SECRET, etc.)
|
||||||
|
|
||||||
|
**No hardcoded test values remain in the codebase.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Last updated: 2026-01-07_
|
||||||
+23
-2
@@ -1,8 +1,29 @@
|
|||||||
|
|
||||||
import { config } from 'dotenv';
|
|
||||||
import { Client } from '@microsoft/microsoft-graph-client';
|
import { Client } from '@microsoft/microsoft-graph-client';
|
||||||
|
|
||||||
config({ path: '/home/admin/secrets/.env' });
|
// Bun-native env loader
|
||||||
|
async function loadEnv(filePath: string) {
|
||||||
|
const file = Bun.file(filePath);
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
console.warn(`⚠ Env file not found: ${filePath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = await file.text();
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const eqIndex = trimmed.indexOf('=');
|
||||||
|
if (eqIndex === -1) continue;
|
||||||
|
const key = trimmed.slice(0, eqIndex).trim();
|
||||||
|
let value = trimmed.slice(eqIndex + 1).trim();
|
||||||
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadEnv('/home/admin/secrets/.env');
|
||||||
|
|
||||||
async function getGraphToken() {
|
async function getGraphToken() {
|
||||||
const tokenEndpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
|
const tokenEndpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"hono": "^4.10.8",
|
"hono": "^4.10.8",
|
||||||
"pocketbase": "^0.26.5"
|
"pocketbase": "^0.26.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,20 +23,30 @@ const CONFIG = {
|
|||||||
TEST: {
|
TEST: {
|
||||||
pbCollection: 'Job_Info_TestEnv',
|
pbCollection: 'Job_Info_TestEnv',
|
||||||
excelTable: 'Test_Table',
|
excelTable: 'Test_Table',
|
||||||
|
plannerGroupId: 'e45f2188-dc65-42d6-b7b0-d0a873e07472',
|
||||||
|
plannerPlanId: '87nMEU5OqUqro1xLjY58-2UAD4uj',
|
||||||
|
plannerBucketId: '9VuH0OQEa0GQsCHtPgUCFmUACHVF',
|
||||||
description: '🧪 TEST ENVIRONMENT'
|
description: '🧪 TEST ENVIRONMENT'
|
||||||
},
|
},
|
||||||
PROD: {
|
PROD: {
|
||||||
pbCollection: process.env.PB_COLLECTION || 'Job_Info_Prod',
|
pbCollection: process.env.PB_COLLECTION || 'Job_Info_Prod',
|
||||||
excelTable: process.env.EXCEL_TABLE_NAME || 'Job_List',
|
excelTable: process.env.EXCEL_TABLE_NAME || 'Job_List',
|
||||||
|
plannerGroupId: process.env.PLANNER_GROUP_ID || 'e45f2188-dc65-42d6-b7b0-d0a873e07472',
|
||||||
|
plannerPlanId: process.env.PLANNER_PLAN_ID || '87nMEU5OqUqro1xLjY58-2UAD4uj',
|
||||||
|
plannerBucketId: process.env.PLANNER_BUCKET_ID || '9VuH0OQEa0GQsCHtPgUCFmUACHVF',
|
||||||
description: '🚀 PRODUCTION ENVIRONMENT'
|
description: '🚀 PRODUCTION ENVIRONMENT'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeConfig = MODE === 'PROD' ? CONFIG.PROD : CONFIG.TEST;
|
const activeConfig = MODE === 'PROD' ? CONFIG.PROD : CONFIG.TEST;
|
||||||
console.log('\n' + '*'.repeat(60));
|
console.log('\n' + '*'.repeat(60));
|
||||||
console.log(' RUNNING IN: ' + activeConfig.description);
|
console.log(' RUNNING IN: ' + activeConfig.description);
|
||||||
console.log(' PocketBase: ' + activeConfig.pbCollection);
|
console.log(' PocketBase: ' + activeConfig.pbCollection);
|
||||||
console.log(' Excel Table: ' + activeConfig.excelTable);
|
console.log(' Excel Table: ' + activeConfig.excelTable);
|
||||||
|
console.log(' Planner Plan: ' + activeConfig.plannerPlanId);
|
||||||
|
console.log(' Planner Bucket: ' + activeConfig.plannerBucketId);
|
||||||
|
console.log(' Planner Plan: ' + activeConfig.plannerPlanId);
|
||||||
|
console.log(' Planner Bucket: ' + activeConfig.plannerBucketId);
|
||||||
console.log('*'.repeat(60) + '\n');
|
console.log('*'.repeat(60) + '\n');
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -436,29 +446,46 @@ async function createPlannerTask({
|
|||||||
// Task title: "Disposition New Job Folder for [Job_Number] [Job_Full_Name]"
|
// Task title: "Disposition New Job Folder for [Job_Number] [Job_Full_Name]"
|
||||||
const title = `Disposition New Job Folder for ${jobNumber} ${jobFullName}`;
|
const title = `Disposition New Job Folder for ${jobNumber} ${jobFullName}`;
|
||||||
|
|
||||||
// Create the Planner task
|
// Create the Planner task
|
||||||
const groupId = process.env.PLANNER_GROUP_ID;
|
const groupId = activeConfig.plannerGroupId;
|
||||||
const planId = process.env.PLANNER_PLAN_ID;
|
const planId = activeConfig.plannerPlanId;
|
||||||
const bucketId = process.env.PLANNER_BUCKET_ID;
|
const bucketId = activeConfig.plannerBucketId;
|
||||||
|
|
||||||
if (!groupId || !planId || !bucketId) {
|
if (!groupId || !planId || !bucketId) {
|
||||||
throw new Error('Missing Planner configuration: PLANNER_GROUP_ID, PLANNER_PLAN_ID, or PLANNER_BUCKET_ID');
|
throw new Error('Missing Planner configuration in activeConfig');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTask = await client.api('/planner/tasks').post({
|
const newTask = await client.api('/planner/tasks').post({
|
||||||
planId,
|
planId,
|
||||||
bucketId,
|
bucketId,
|
||||||
title,
|
title,
|
||||||
assignments: {
|
assignments: {
|
||||||
[assigneeId]: {
|
[assigneeId]: {
|
||||||
'@odata.type': 'microsoft.graph.plannerAssignment',
|
'@odata.type': 'microsoft.graph.plannerAssignment',
|
||||||
orderHint: ' !',
|
orderHint: ' !',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
...(dueDateObj ? { dueDateTime: dueDateObj } : {}),
|
||||||
...(dueDateObj ? { dueDateTime: dueDateObj } : {}),
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Planner task created in Plan ID: ${planId}`);
|
||||||
|
|
||||||
|
console.log(`✓ Planner task created in Plan ID: ${planId}`);
|
||||||
|
|
||||||
|
// Build checklist object with unique IDs
|
||||||
|
const checklist: Record<string, { '@odata.type': string; title: string; isChecked: boolean; orderHint: string }> = {};
|
||||||
|
const checklistItems = ['Create Voxer Chat', 'Create Quickbooks', 'Upload plans'];
|
||||||
|
checklistItems.forEach((item, index) => {
|
||||||
|
const itemId = `checklist_${Date.now()}_${index}`;
|
||||||
|
checklist[itemId] = {
|
||||||
|
'@odata.type': 'microsoft.graph.plannerChecklistItem',
|
||||||
|
title: item,
|
||||||
|
isChecked: false,
|
||||||
|
orderHint: ` ${index}!`,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the Notes/Description via task details (match the working test script)
|
// Set the Notes/Description AND checklist via task details
|
||||||
const taskDetails = await client.api(`/planner/tasks/${newTask.id}/details`).get();
|
const taskDetails = await client.api(`/planner/tasks/${newTask.id}/details`).get();
|
||||||
const detailsEtag = taskDetails['@odata.etag'];
|
const detailsEtag = taskDetails['@odata.etag'];
|
||||||
console.log(`[Planner] Task created ${newTask.id}, fetched details ETag: ${detailsEtag}`);
|
console.log(`[Planner] Task created ${newTask.id}, fetched details ETag: ${detailsEtag}`);
|
||||||
@@ -467,8 +494,22 @@ async function createPlannerTask({
|
|||||||
.header('If-Match', detailsEtag)
|
.header('If-Match', detailsEtag)
|
||||||
.patch({
|
.patch({
|
||||||
description: 'Move the new folder to the correct Job Pages folder.',
|
description: 'Move the new folder to the correct Job Pages folder.',
|
||||||
|
checklist: checklist,
|
||||||
});
|
});
|
||||||
console.log(`[Planner] Successfully patched description`);
|
console.log(`[Planner] Successfully patched description and added ${checklistItems.length} checklist items`);
|
||||||
|
|
||||||
|
// Add "plum" label (category6) to task
|
||||||
|
const taskForLabel = await client.api(`/planner/tasks/${newTask.id}`).get();
|
||||||
|
const taskEtag = taskForLabel['@odata.etag'];
|
||||||
|
await client
|
||||||
|
.api(`/planner/tasks/${newTask.id}`)
|
||||||
|
.header('If-Match', taskEtag)
|
||||||
|
.patch({
|
||||||
|
appliedCategories: {
|
||||||
|
category6: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`[Planner] Added plum label to task`);
|
||||||
|
|
||||||
console.log(`✓ Planner task created: "${title}" assigned to ${assigneeName}`);
|
console.log(`✓ Planner task created: "${title}" assigned to ${assigneeName}`);
|
||||||
return { success: true, taskId: newTask.id, title };
|
return { success: true, taskId: newTask.id, title };
|
||||||
|
|||||||
Reference in New Issue
Block a user