v1.0.0-beta3: Teams notifications, Excel formula preservation
This commit is contained in:
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
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-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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Uses the same Graph API token from the server to avoid re-authentication
|
// Uses the same Graph API token from the server to avoid re-authentication
|
||||||
|
|
||||||
const { DRIVE_ID, PARENT_ITEM_ID } = require('./config');
|
const { DRIVE_ID, PARENT_ITEM_ID } = require('./config');
|
||||||
const { Document, Packer, Paragraph, HeadingLevel, convertInchesToTwip } = require('docx');
|
const { Document, Packer, Paragraph, HeadingLevel, TextRun, convertInchesToTwip } = require('docx');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a manager info document template
|
* Creates a manager info document template
|
||||||
@@ -45,27 +45,45 @@ async function createManagerInfoDocument(jobNumber, jobName, record) {
|
|||||||
spacing: { before: 200, after: 100 }
|
spacing: { before: 200, after: 100 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Job Name: ${jobName}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Job Name: ' }),
|
||||||
|
new TextRun({ text: jobName, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 50 }
|
spacing: { after: 50 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Job Address: ${jobAddress}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Job Address: ' }),
|
||||||
|
new TextRun({ text: jobAddress, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 50 }
|
spacing: { after: 50 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Job Division: ${jobDivision}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Job Division: ' }),
|
||||||
|
new TextRun({ text: jobDivision, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 50 }
|
spacing: { after: 50 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Customer/Client Name: ${companyClient}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Customer/Client Name: ' }),
|
||||||
|
new TextRun({ text: companyClient, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 50 }
|
spacing: { after: 50 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Job Type: ${jobType}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Job Type: ' }),
|
||||||
|
new TextRun({ text: jobType, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 50 }
|
spacing: { after: 50 }
|
||||||
}),
|
}),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `Job Status: ${jobStatus}`,
|
children: [
|
||||||
|
new TextRun({ text: 'Job Status: ' }),
|
||||||
|
new TextRun({ text: jobStatus, underline: {} })
|
||||||
|
],
|
||||||
spacing: { after: 200 }
|
spacing: { after: 200 }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
nohup: ignoring input
|
||||||
|
$ bun run server.ts
|
||||||
|
[dotenv@17.2.3] injecting env (13) from ../secrets/.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
|
||||||
|
Job Creation with Excel Sync server running at http://localhost:4000
|
||||||
|
Started development server: http://localhost:4000
|
||||||
|
✓ Table cache initialized with 47 columns
|
||||||
|
error: script "dev" was terminated by signal SIGTERM (Polite quit request)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
nohup: ignoring input
|
||||||
|
[dotenv@17.2.3] injecting env (13) from ../secrets/.env -- tip: 📡 add observability to secrets: https://dotenvx.com/ops
|
||||||
|
Job Creation with Excel Sync server running at http://localhost:4000
|
||||||
|
Started development server: http://localhost:4000
|
||||||
|
✓ Table cache initialized with 47 columns
|
||||||
|
✓ Created PocketBase record with Job_Number: 4764, ID: 99oq6cdykiuoy2y
|
||||||
|
✓ Updated PB with calculated fields (Full Name, QB, Voxer, Codes)
|
||||||
|
✓ Synced to Excel (pb_id: 99oq6cdykiuoy2y)
|
||||||
|
✓ Excel row created
|
||||||
|
[Post-Record Integration] Processing new job record: {
|
||||||
|
jobNumber: "4764",
|
||||||
|
jobName: "Again",
|
||||||
|
recordId: "99oq6cdykiuoy2y",
|
||||||
|
}
|
||||||
|
[Post-Record Integration] Calculated job full name: Again - jlk - abs
|
||||||
|
[Job Folder Integration] Creating main folder: 4764 - Again - jlk - abs
|
||||||
|
[Job Folder Integration] Create folder response: {
|
||||||
|
status: 201,
|
||||||
|
folderId: "01SPNXLDT4VWRLN7EOKVCJBKYAUJD5WAZL",
|
||||||
|
folderName: "4764 - Again - jlk - abs",
|
||||||
|
}
|
||||||
|
[Job Folder Integration] Creating subfolder: Managers Info
|
||||||
|
[Job Folder Integration] Create subfolder response: {
|
||||||
|
status: 201,
|
||||||
|
subfolderId: "01SPNXLDWATR7EERSJTJAIF2BT3V5NBKDM",
|
||||||
|
}
|
||||||
|
[Job Folder Integration] Creating and uploading Manager Info document
|
||||||
|
[Job Folder Integration] ✓ Manager Info document uploaded successfully
|
||||||
|
[Job Folder Integration] Creating share link for main folder
|
||||||
|
[Job Folder Integration] Share link response: {
|
||||||
|
status: 201,
|
||||||
|
hasLink: true,
|
||||||
|
}
|
||||||
|
[Job Folder Integration] ✓ Successfully created folder structure and share link
|
||||||
|
[Job Folder Integration] Share link: https://czflex.sharepoint.com/:f:/s/Team/IgB8raK2_I5VRJCrAKJH2wMrAbVvY7DZ4ctPRR_40vi5mJs
|
||||||
|
[Post-Record Integration] ✓ Folder creation successful
|
||||||
|
[Post-Record Integration] Share link: https://czflex.sharepoint.com/:f:/s/Team/IgB8raK2_I5VRJCrAKJH2wMrAbVvY7DZ4ctPRR_40vi5mJs
|
||||||
|
✓ Folder created and share link captured: https://czflex.sharepoint.com/:f:/s/Team/IgB8raK2_I5VRJCrAKJH2wMrAbVvY7DZ4ctPRR_40vi5mJs
|
||||||
|
📁 Open folder: https://czflex.sharepoint.com/:f:/s/Team/IgB8raK2_I5VRJCrAKJH2wMrAbVvY7DZ4ctPRR_40vi5mJs
|
||||||
|
✓ Updated Excel Job_Folder_Link for pb_id: 99oq6cdykiuoy2y
|
||||||
|
✓ Updated PB with folder link, due date counter (static), and active (boolean)
|
||||||
|
[Teams Webhook] Sending notification for Job 4764
|
||||||
|
⚠️ Teams notification failed: 400 Bad Request
|
||||||
|
Response: Summary or Text is required.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "job-creation-with-excel-sync",
|
"name": "job-creation-with-excel-sync",
|
||||||
"version": "1.0.0-beta2",
|
"version": "1.0.0-beta3",
|
||||||
"description": "Unified job creation form with PocketBase and Excel sync",
|
"description": "Unified job creation form with PocketBase and Excel sync",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -54,81 +54,101 @@ async function logError(context: string, error: any, additionalData?: any) {
|
|||||||
// TEAMS WEBHOOK NOTIFICATION (Power Automate)
|
// TEAMS WEBHOOK NOTIFICATION (Power Automate)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function buildAdaptiveCard(jobData: any, jobFolderLink: string) {
|
function toIsoDateOnly(val: any): string {
|
||||||
const jobName = jobData.Job_Name || '';
|
if (!val) return '';
|
||||||
const jobNumber = jobData.Job_Number || '';
|
const d = new Date(val);
|
||||||
const jobAddress = jobData.Job_Address || '';
|
if (isNaN(d.getTime())) return '';
|
||||||
const companyClient = jobData.Company_Client || '';
|
const yyyy = d.getUTCFullYear();
|
||||||
const jobDivision = jobData.Job_Division || '';
|
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||||
const jobType = jobData.Job_Type || '';
|
const dd = String(d.getUTCDate()).padStart(2, '0');
|
||||||
const contactPerson = jobData.Contact_Person || '';
|
return `${yyyy}-${mm}-${dd}`;
|
||||||
const phoneNumber = jobData.Phone_Number || '';
|
}
|
||||||
|
|
||||||
// Valid Adaptive Card (Teams flowbot expects type: AdaptiveCard)
|
function buildPowerAutomateExcelItem(record: any) {
|
||||||
return {
|
return {
|
||||||
type: 'AdaptiveCard',
|
'@odata.context': '',
|
||||||
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
'@odata.etag': '',
|
||||||
version: '1.5',
|
ItemInternalId: record.id || '',
|
||||||
body: [
|
'Job_Number': record.Job_Number || record.job_number || '',
|
||||||
{ type: 'TextBlock', text: 'New Job!', weight: 'Bolder', size: 'Large' },
|
'Job_Name': record.Job_Name || record.job_name || '',
|
||||||
{ type: 'TextBlock', text: `${jobNumber} - ${jobName}`, wrap: true },
|
'Job Address': record.Job_Address || record.job_address || '',
|
||||||
{
|
'Job Type': record.Job_Type || record.job_type || '',
|
||||||
type: 'FactSet',
|
'Job Status': record.Job_Status || record.job_status || '',
|
||||||
facts: [
|
'Job Division': record.Job_Division || record.job_division || '',
|
||||||
{ title: 'Number', value: jobNumber || 'N/A' },
|
'Estimator': record.Estimator || record.estimator || '',
|
||||||
{ title: 'Name', value: jobName || 'N/A' },
|
'Office Rep': record.Office_Rep || record.office_rep || '',
|
||||||
{ title: 'Address', value: jobAddress || 'N/A' },
|
'Manager': record.Manager || record.manager || '',
|
||||||
{ title: 'Client', value: companyClient || 'N/A' },
|
'Company/Client': record.Company_Client || record.company_client || '',
|
||||||
{ title: 'Division', value: jobDivision || 'N/A' },
|
'Contact Person': record.Contact_Person || record.contact_person || '',
|
||||||
{ title: 'Type', value: jobType || 'N/A' },
|
'Phone Number': record.Phone_Number || record.phone_number || '',
|
||||||
{ title: 'Contact', value: (contactPerson && phoneNumber) ? `${contactPerson} - ${phoneNumber}` : (contactPerson || 'N/A') }
|
'Email': record.Email || record.email || '',
|
||||||
]
|
'Due Date': toIsoDateOnly(record.Due_Date || record.due_date || ''),
|
||||||
}
|
'Due Date Source': record.Due_Date_Source || record.due_date_source || '',
|
||||||
],
|
'Due Time': record.Due_Time || record.due_time || '',
|
||||||
actions: jobFolderLink ? [
|
'Start Date': toIsoDateOnly(record.Start_Date || record.start_date || '')
|
||||||
{ type: 'Action.OpenUrl', title: 'Open Folder', url: jobFolderLink }
|
|
||||||
] : []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notifyTeamsChannel(jobData: any, jobFolderLink: string) {
|
function buildAdaptiveCard(jobData: any) {
|
||||||
const webhook = process.env.POWER_AUTOMATE_WEBHOOK_URL || process.env.TEAMS_WEBHOOK_URL;
|
const jobName = jobData.Job_Name || '';
|
||||||
|
const jobNumber = jobData.Job_Number || '';
|
||||||
|
const jobAddress = jobData.Job_Address || '';
|
||||||
|
const jobDivision = jobData.Job_Division || '';
|
||||||
|
|
||||||
|
// Format timestamp for the card
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
||||||
|
type: 'AdaptiveCard',
|
||||||
|
version: '1.4',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'Container',
|
||||||
|
style: 'emphasis',
|
||||||
|
items: [
|
||||||
|
{ type: 'TextBlock', text: 'New Job!', weight: 'Bolder', size: 'Large', spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: `**Number:** ${jobNumber || 'N/A'}`, spacing: 'Small' },
|
||||||
|
{ type: 'TextBlock', text: `**Name:** ${jobName || 'N/A'}`, spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: `**Address:** ${jobAddress || 'N/A'}`, spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: `**Division:** ${jobDivision || 'N/A'}`, spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: timestamp, size: 'Small', isSubtle: true, spacing: 'Small' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyTeamsChannel(jobData: any) {
|
||||||
|
const webhook = process.env.TEAMS_WEBHOOK_URL;
|
||||||
|
|
||||||
// Skip if webhook not configured
|
|
||||||
if (!webhook) {
|
if (!webhook) {
|
||||||
console.log('⚠️ Power Automate webhook not configured (POWER_AUTOMATE_WEBHOOK_URL missing)');
|
console.log('⚠️ TEAMS_WEBHOOK_URL not configured');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jobName = jobData.Job_Name || '';
|
const adaptiveCard = buildAdaptiveCard(jobData);
|
||||||
const jobNumber = jobData.Job_Number || '';
|
|
||||||
const jobAddress = jobData.Job_Address || '';
|
|
||||||
const companyClient = jobData.Company_Client || '';
|
|
||||||
const jobDivision = jobData.Job_Division || '';
|
|
||||||
const jobType = jobData.Job_Type || '';
|
|
||||||
const contactPerson = jobData.Contact_Person || '';
|
|
||||||
const phoneNumber = jobData.Phone_Number || '';
|
|
||||||
|
|
||||||
// Build Adaptive Card and payload for Power Automate
|
|
||||||
const adaptiveCard = buildAdaptiveCard(jobData, jobFolderLink);
|
|
||||||
const payload = {
|
const payload = {
|
||||||
// raw fields, in case the flow maps them directly
|
type: 'message',
|
||||||
jobNumber,
|
attachments: [
|
||||||
jobName,
|
{
|
||||||
jobAddress,
|
contentType: 'application/vnd.microsoft.card.adaptive',
|
||||||
companyClient,
|
contentUrl: null,
|
||||||
jobDivision,
|
content: adaptiveCard
|
||||||
jobType,
|
}
|
||||||
contactPerson,
|
]
|
||||||
phoneNumber,
|
|
||||||
jobFolderLink,
|
|
||||||
// Provide card in common parameter names used by PA templates
|
|
||||||
card: JSON.stringify(adaptiveCard),
|
|
||||||
adaptiveCard
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[Power Automate] Sending webhook for Job ${jobNumber}:`, JSON.stringify(payload, null, 2));
|
console.log(`[Teams Webhook] Sending notification for Job ${jobData.Job_Number || ''}:`, JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
const response = await fetch(webhook, {
|
const response = await fetch(webhook, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -140,14 +160,13 @@ async function notifyTeamsChannel(jobData: any, jobFolderLink: string) {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
console.warn(`⚠️ Power Automate notification failed: ${response.status} ${response.statusText}`);
|
console.warn(`⚠️ Teams notification failed: ${response.status} ${response.statusText}`);
|
||||||
console.warn(` Response: ${text}`);
|
console.warn(` Response: ${text}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`✓ Power Automate notification sent for Job ${jobNumber} (${response.status})`);
|
console.log(`✓ Teams notification sent for Job ${jobData.Job_Number || ''} (${response.status})`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('⚠️ Failed to send Power Automate notification:', error.message);
|
console.error('⚠️ Failed to send Teams notification:', error.message);
|
||||||
// Don't fail the job creation if notification fails
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,16 +545,15 @@ async function updateExcelFolderLink(pbId: string, link: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update using table row itemAt - update the full row with link populated
|
// Update only the Job_Folder_Link cell to preserve formulas in other columns (e.g., Active)
|
||||||
const currentValues = [...target.values[0]]; // Copy array
|
// Use the table row's range and cell() method to target just the one cell
|
||||||
currentValues[linkColIndex] = link || '';
|
// This avoids hardcoding worksheet names and works through the table structure
|
||||||
|
await client.api(`${excelTablePath}/rows/itemAt(index=${target.index})/range/cell(row=0,column=${linkColIndex})`)
|
||||||
await client.api(`${excelTablePath}/rows/itemAt(index=${target.index})`)
|
|
||||||
.patch({
|
.patch({
|
||||||
values: [currentValues]
|
values: [[link || '']]
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✓ Updated Excel Job_Folder_Link for pb_id:', pbId);
|
console.log('✓ Updated Excel Job_Folder_Link cell for pb_id:', pbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API endpoint to submit job
|
// API endpoint to submit job
|
||||||
@@ -702,11 +720,9 @@ app.post('/api/submit', async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send Teams notification (fire and forget, don't block response)
|
// Send Teams notification (fire and forget, don't block response)
|
||||||
if (jobFolderLink) {
|
notifyTeamsChannel(record).catch(err => {
|
||||||
notifyTeamsChannel(record, jobFolderLink).catch(err => {
|
|
||||||
console.error('⚠️ Teams notification error:', err.message);
|
console.error('⚠️ Teams notification error:', err.message);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -737,6 +753,171 @@ app.get('/api/health', (c) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test route: sends an exact sample Adaptive Card payload (no envelope)
|
||||||
|
app.post('/api/test-notification', async (c) => {
|
||||||
|
const webhook = process.env.POWER_AUTOMATE_WEBHOOK_URL || process.env.TEAMS_WEBHOOK_URL;
|
||||||
|
if (!webhook) {
|
||||||
|
return c.json({ success: false, message: 'Webhook not configured' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
||||||
|
type: 'AdaptiveCard',
|
||||||
|
version: '1.4',
|
||||||
|
body: [
|
||||||
|
{ type: 'TextBlock', text: 'New Job!', weight: 'Bolder', size: 'Large' },
|
||||||
|
{ type: 'TextBlock', text: '4766 - Teams Card v1.4', wrap: true },
|
||||||
|
{
|
||||||
|
type: 'FactSet',
|
||||||
|
facts: [
|
||||||
|
{ title: 'Number', value: '4766' },
|
||||||
|
{ title: 'Name', value: 'Teams Card v1.4' },
|
||||||
|
{ title: 'Address', value: 'Blue eye' },
|
||||||
|
{ title: 'Client', value: 'ClientCo' },
|
||||||
|
{ title: 'Division', value: 'C#' },
|
||||||
|
{ title: 'Type', value: 'PWEX' },
|
||||||
|
{ title: 'Contact', value: 'Wyatt Gann - (417) 429-1417 x117' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'Action.OpenUrl',
|
||||||
|
title: 'Open Folder',
|
||||||
|
url: 'https://czflex.sharepoint.com/:f:/s/Team/IgCD37kQLFb6Q7BsoWjFb5rlAWkqMkqyX2zhoQpRMh9hUiI'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch(webhook, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = resp.ok;
|
||||||
|
const text = await resp.text();
|
||||||
|
return c.json({ success: ok, status: resp.status, body: text });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test route: sends the same sample card wrapped in a Teams message envelope
|
||||||
|
app.post('/api/test-notification-envelope', async (c) => {
|
||||||
|
const webhook = process.env.POWER_AUTOMATE_WEBHOOK_URL || process.env.TEAMS_WEBHOOK_URL;
|
||||||
|
if (!webhook) {
|
||||||
|
return c.json({ success: false, message: 'Webhook not configured' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
||||||
|
type: 'AdaptiveCard',
|
||||||
|
version: '1.4',
|
||||||
|
body: [
|
||||||
|
{ type: 'TextBlock', text: 'New Job!', weight: 'Bolder', size: 'Large' },
|
||||||
|
{ type: 'TextBlock', text: '4766 - Teams Card v1.4', wrap: true },
|
||||||
|
{
|
||||||
|
type: 'FactSet',
|
||||||
|
facts: [
|
||||||
|
{ title: 'Number', value: '4766' },
|
||||||
|
{ title: 'Name', value: 'Teams Card v1.4' },
|
||||||
|
{ title: 'Address', value: 'Blue eye' },
|
||||||
|
{ title: 'Client', value: 'ClientCo' },
|
||||||
|
{ title: 'Division', value: 'C#' },
|
||||||
|
{ title: 'Type', value: 'PWEX' },
|
||||||
|
{ title: 'Contact', value: 'Wyatt Gann - (417) 429-1417 x117' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'Action.OpenUrl',
|
||||||
|
title: 'Open Folder',
|
||||||
|
url: 'https://czflex.sharepoint.com/:f:/s/Team/IgCD37kQLFb6Q7BsoWjFb5rlAWkqMkqyX2zhoQpRMh9hUiI'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'message',
|
||||||
|
text: 'New Job!',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'application/vnd.microsoft.card.adaptive',
|
||||||
|
content: card
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch(webhook, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = resp.ok;
|
||||||
|
const text = await resp.text();
|
||||||
|
return c.json({ success: ok, status: resp.status, body: text });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test route: post exact sample card directly to Teams Incoming Webhook (no envelope)
|
||||||
|
app.post('/api/test-teams-webhook', async (c) => {
|
||||||
|
const webhook = process.env.TEAMS_WEBHOOK_URL;
|
||||||
|
if (!webhook) {
|
||||||
|
return c.json({ success: false, message: 'TEAMS_WEBHOOK_URL not configured' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format timestamp for the card
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const adaptiveCard = {
|
||||||
|
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
||||||
|
type: 'AdaptiveCard',
|
||||||
|
version: '1.4',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'Container',
|
||||||
|
style: 'emphasis',
|
||||||
|
items: [
|
||||||
|
{ type: 'TextBlock', text: 'New Job!', weight: 'Bolder', size: 'Large', spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: '**Number:** 4738', spacing: 'Small' },
|
||||||
|
{ type: 'TextBlock', text: '**Name:** New VO-AG Facility & Early Childhood Addition', spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: '**Address:** Blue eye', spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: '**Division:** C#', spacing: 'None' },
|
||||||
|
{ type: 'TextBlock', text: timestamp, size: 'Small', isSubtle: true, spacing: 'Small' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'message',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'application/vnd.microsoft.card.adaptive',
|
||||||
|
contentUrl: null,
|
||||||
|
content: adaptiveCard
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch(webhook, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = resp.ok;
|
||||||
|
const text = await resp.text();
|
||||||
|
return c.json({ success: ok, status: resp.status, body: text });
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT || 4000);
|
const PORT = Number(process.env.PORT || 4000);
|
||||||
|
|
||||||
console.log(`Job Creation with Excel Sync server running at http://localhost:${PORT}`);
|
console.log(`Job Creation with Excel Sync server running at http://localhost:${PORT}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user