email templates enhanced handlebars added tfor better comaptibility and interview evaluation updated with new keys to store approve/reject status
This commit is contained in:
parent
31109d6109
commit
113e87b66d
64
package-lock.json
generated
64
package-lock.json
generated
@ -16,11 +16,12 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
"express-validator": "^7.3.1",
|
"express-validator": "^7.3.1",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"pg": "^8.17.2",
|
"pg": "^8.18.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@ -5578,6 +5579,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/handlebars": {
|
||||||
|
"version": "4.7.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||||
|
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.2",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@ -7114,6 +7136,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/neo-async": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -7481,12 +7509,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.17.2",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||||
"integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==",
|
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.10.1",
|
"pg-connection-string": "^2.11.0",
|
||||||
"pg-pool": "^3.11.0",
|
"pg-pool": "^3.11.0",
|
||||||
"pg-protocol": "^1.11.0",
|
"pg-protocol": "^1.11.0",
|
||||||
"pg-types": "2.2.0",
|
"pg-types": "2.2.0",
|
||||||
@ -7515,9 +7543,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/pg-connection-string": {
|
"node_modules/pg-connection-string": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
|
||||||
"integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==",
|
"integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-hstore": {
|
"node_modules/pg-hstore": {
|
||||||
@ -8287,7 +8315,6 @@
|
|||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -8875,6 +8902,19 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
|
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undefsafe": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
@ -9099,6 +9139,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
|||||||
@ -31,11 +31,12 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
"express-validator": "^7.3.1",
|
"express-validator": "^7.3.1",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"pg": "^8.17.2",
|
"pg": "^8.18.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@ -66,4 +67,4 @@
|
|||||||
"node": ">=18.0.0",
|
"node": ">=18.0.0",
|
||||||
"npm": ">=9.0.0"
|
"npm": ">=9.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
scripts/add-decision-column.ts
Normal file
26
scripts/add-decision-column.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: console.log
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
console.log('Adding decision column to interview_evaluations table...');
|
||||||
|
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "decision" VARCHAR(255);');
|
||||||
|
|
||||||
|
console.log('Column added successfully.');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
34
scripts/debug-evaluations.ts
Normal file
34
scripts/debug-evaluations.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
const [results] = await sequelize.query(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'interview_evaluations';
|
||||||
|
`);
|
||||||
|
console.log('Columns in interview_evaluations:');
|
||||||
|
console.table(results);
|
||||||
|
|
||||||
|
const [evals] = await sequelize.query('SELECT * FROM "interview_evaluations" ORDER BY "createdAt" DESC LIMIT 1;');
|
||||||
|
console.log('Latest evaluation:');
|
||||||
|
console.log(evals[0]);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
26
scripts/fix-remarks-column.ts
Normal file
26
scripts/fix-remarks-column.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: console.log
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
console.log('Adding remarks column to interview_evaluations table...');
|
||||||
|
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "remarks" TEXT;');
|
||||||
|
|
||||||
|
console.log('Column added successfully.');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
47
scripts/fix-stages-enum.ts
Normal file
47
scripts/fix-stages-enum.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: console.log
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
const stagesToAdd = [
|
||||||
|
'Level 1 Approved',
|
||||||
|
'Level 2 Approved',
|
||||||
|
'Level 2 Recommended',
|
||||||
|
'Level 3 Approved'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const stage of stagesToAdd) {
|
||||||
|
try {
|
||||||
|
await sequelize.query(`ALTER TYPE "enum_applications_currentStage" ADD VALUE IF NOT EXISTS '${stage}';`);
|
||||||
|
console.log(`Added '${stage}' to enum_applications_currentStage`);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`'${stage}' might already exist in enum_applications_currentStage or error:`, e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS '${stage}';`);
|
||||||
|
console.log(`Added '${stage}' to enum_applications_overallStatus`);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`'${stage}' might already exist in enum_applications_overallStatus or error:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully updated enums.');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
29
scripts/migrate-evaluation-schema.ts
Normal file
29
scripts/migrate-evaluation-schema.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: console.log
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
console.log('Renaming recommendation to decision and remarks to decisionRemarks in interview_evaluations...');
|
||||||
|
|
||||||
|
// Use a transaction for safety
|
||||||
|
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "recommendation" TO "decision";');
|
||||||
|
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "remarks" TO "decisionRemarks";');
|
||||||
|
|
||||||
|
console.log('Columns renamed successfully.');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during migration:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
@ -35,6 +35,10 @@ export const APPLICATION_STAGES = {
|
|||||||
NBH: 'NBH',
|
NBH: 'NBH',
|
||||||
LEGAL: 'Legal',
|
LEGAL: 'Legal',
|
||||||
FINANCE: 'Finance',
|
FINANCE: 'Finance',
|
||||||
|
LEVEL_1_APPROVED: 'Level 1 Approved',
|
||||||
|
LEVEL_2_APPROVED: 'Level 2 Approved',
|
||||||
|
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
|
||||||
|
LEVEL_3_APPROVED: 'Level 3 Approved',
|
||||||
APPROVED: 'Approved',
|
APPROVED: 'Approved',
|
||||||
REJECTED: 'Rejected'
|
REJECTED: 'Rejected'
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@ -43,22 +44,22 @@ export const sendEmail = async (to: string, subject: string, templateCode: strin
|
|||||||
const dbTemplate = await EmailTemplate.findOne({ where: { templateCode, isActive: true } });
|
const dbTemplate = await EmailTemplate.findOne({ where: { templateCode, isActive: true } });
|
||||||
|
|
||||||
if (dbTemplate) {
|
if (dbTemplate) {
|
||||||
finalHtml = dbTemplate.body;
|
// Prepare replacements with extra global vars
|
||||||
finalSubject = dbTemplate.subject;
|
|
||||||
|
|
||||||
// Replace placeholders in DB template
|
|
||||||
const allReplacements = {
|
const allReplacements = {
|
||||||
...replacements,
|
...replacements,
|
||||||
year: new Date().getFullYear().toString()
|
year: new Date().getFullYear().toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key in allReplacements) {
|
// Compile subject and body with data using Handlebars
|
||||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
const subjectTemplate = handlebars.compile(dbTemplate.subject);
|
||||||
finalHtml = finalHtml.replace(regex, (allReplacements as any)[key]);
|
finalSubject = subjectTemplate(allReplacements);
|
||||||
finalSubject = finalSubject.replace(regex, (allReplacements as any)[key]);
|
|
||||||
}
|
const bodyTemplate = handlebars.compile(dbTemplate.body);
|
||||||
|
finalHtml = bodyTemplate(allReplacements);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to local file
|
// Fallback to local file
|
||||||
|
// Note: Local files are simple replacements for now, or could also be updated to handlebars if needed
|
||||||
|
// For now keeping readTemplate but we should ideally migrate local templates too if they get complex
|
||||||
const localHtml = readTemplate(templateCode, {
|
const localHtml = readTemplate(templateCode, {
|
||||||
...replacements,
|
...replacements,
|
||||||
year: new Date().getFullYear().toString()
|
year: new Date().getFullYear().toString()
|
||||||
@ -83,6 +84,9 @@ export const sendEmail = async (to: string, subject: string, templateCode: strin
|
|||||||
html: finalHtml
|
html: finalHtml
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[Email Service] Email sent to ${to}. MessageId: ${info.messageId}`);
|
||||||
|
console.log(`[Email Service] Preview URL: ${nodemailer.getTestMessageUrl(info)}`);
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to send email (${templateCode}):`, error);
|
console.error(`Failed to send email (${templateCode}):`, error);
|
||||||
@ -107,13 +111,19 @@ export const sendNonOpportunityEmail = async (to: string, applicantName: string,
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sendInterviewScheduledEmail = async (to: string, name: string, applicationId: string, interview: any) => {
|
export const sendInterviewScheduledEmail = async (to: string, name: string, applicationId: string, interview: any) => {
|
||||||
|
const date = new Date(interview.scheduleDate);
|
||||||
|
const time = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
|
||||||
await sendEmail(to, `Interview Scheduled: ${applicationId}`, 'INTERVIEW_SCHEDULED', {
|
await sendEmail(to, `Interview Scheduled: ${applicationId}`, 'INTERVIEW_SCHEDULED', {
|
||||||
name,
|
applicant_name: name,
|
||||||
applicationId,
|
application_id: applicationId,
|
||||||
level: interview.level,
|
level: interview.level,
|
||||||
dateTime: new Date(interview.scheduledAt).toLocaleString(),
|
interview_date: formattedDate,
|
||||||
type: interview.type,
|
interview_time: time,
|
||||||
location: interview.location
|
type: interview.interviewType,
|
||||||
|
location: interview.linkOrLocation,
|
||||||
|
status: interview.status
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
132
src/controllers/admin/EmailTemplateController.ts
Normal file
132
src/controllers/admin/EmailTemplateController.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import db from '../../database/models/index.js';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
|
||||||
|
const { EmailTemplate } = db;
|
||||||
|
|
||||||
|
export const EmailTemplateController = {
|
||||||
|
// Get all templates
|
||||||
|
getAllTemplates: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const templates = await EmailTemplate.findAll({
|
||||||
|
order: [['templateCode', 'ASC']]
|
||||||
|
});
|
||||||
|
res.json({ success: true, data: templates });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching templates:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to fetch templates' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get single template
|
||||||
|
getTemplate: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const template = await EmailTemplate.findByPk(id);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, data: template });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching template:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to fetch template' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create template
|
||||||
|
createTemplate: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const template = await EmailTemplate.create(req.body);
|
||||||
|
res.status(201).json({ success: true, data: template, message: 'Template created successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating template:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to create template' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update template
|
||||||
|
updateTemplate: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const [updated] = await EmailTemplate.update(req.body, {
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
const updatedTemplate = await EmailTemplate.findByPk(id);
|
||||||
|
res.json({ success: true, data: updatedTemplate, message: 'Template updated successfully' });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ success: false, message: 'Template not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating template:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to update template' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete template
|
||||||
|
deleteTemplate: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const deleted = await EmailTemplate.destroy({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
res.json({ success: true, message: 'Template deleted successfully' });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ success: false, message: 'Template not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting template:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to delete template' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Preview template
|
||||||
|
previewTemplate: async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { subject, body, data } = req.body;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Template body is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile content
|
||||||
|
let compiledSubject = subject;
|
||||||
|
let compiledBody = body;
|
||||||
|
|
||||||
|
const safeData = data || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (subject) {
|
||||||
|
const subjectTemplate = handlebars.compile(subject);
|
||||||
|
compiledSubject = subjectTemplate(safeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyTemplate = handlebars.compile(body);
|
||||||
|
compiledBody = bodyTemplate(safeData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
subject: compiledSubject,
|
||||||
|
html: compiledBody
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (compileError: any) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Template compilation failed',
|
||||||
|
error: compileError.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error previewing template:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Failed to preview template' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -12,6 +12,7 @@ export interface DocumentAttributes {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
fileSize: number | null;
|
fileSize: number | null;
|
||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
|
stage: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
uploadedBy: string | null;
|
uploadedBy: string | null;
|
||||||
}
|
}
|
||||||
@ -69,6 +70,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
|
stage: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'active'
|
defaultValue: 'active'
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export interface InterviewEvaluationAttributes {
|
|||||||
ktMatrixScore: number | null;
|
ktMatrixScore: number | null;
|
||||||
qualitativeFeedback: string | null;
|
qualitativeFeedback: string | null;
|
||||||
recommendation: string | null;
|
recommendation: string | null;
|
||||||
|
remarks: string | null;
|
||||||
|
decision: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterviewEvaluationInstance extends Model<InterviewEvaluationAttributes>, InterviewEvaluationAttributes { }
|
export interface InterviewEvaluationInstance extends Model<InterviewEvaluationAttributes>, InterviewEvaluationAttributes { }
|
||||||
@ -45,6 +47,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
recommendation: {
|
recommendation: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
remarks: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'interview_evaluations',
|
tableName: 'interview_evaluations',
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import * as adminController from './admin.controller.js';
|
import * as adminController from './admin.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
import { EmailTemplateController } from '../../controllers/admin/EmailTemplateController.js';
|
||||||
import { checkRole } from '../../common/middleware/roleCheck.js';
|
import { checkRole } from '../../common/middleware/roleCheck.js';
|
||||||
import { ROLES } from '../../common/config/constants.js';
|
import { ROLES } from '../../common/config/constants.js';
|
||||||
|
|
||||||
@ -23,6 +24,14 @@ router.get('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, admi
|
|||||||
router.patch('/users/:id/status', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUserStatus);
|
router.patch('/users/:id/status', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUserStatus);
|
||||||
router.put('/users/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUser);
|
router.put('/users/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUser);
|
||||||
|
|
||||||
|
// Email Templates
|
||||||
|
router.get('/email-templates', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, EmailTemplateController.getAllTemplates);
|
||||||
|
router.get('/email-templates/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, EmailTemplateController.getTemplate);
|
||||||
|
router.post('/email-templates', checkRole([ROLES.SUPER_ADMIN]) as any, EmailTemplateController.createTemplate);
|
||||||
|
router.put('/email-templates/:id', checkRole([ROLES.SUPER_ADMIN]) as any, EmailTemplateController.updateTemplate);
|
||||||
|
router.delete('/email-templates/:id', checkRole([ROLES.SUPER_ADMIN]) as any, EmailTemplateController.deleteTemplate);
|
||||||
|
router.post('/email-templates/preview', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, EmailTemplateController.previewTemplate);
|
||||||
|
|
||||||
// Dealer Codes
|
// Dealer Codes
|
||||||
router.post('/dealer-codes/generate', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.generateDealerCode);
|
router.post('/dealer-codes/generate', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.generateDealerCode);
|
||||||
|
|
||||||
|
|||||||
@ -371,3 +371,180 @@ export const getInterviews = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error fetching interviews' });
|
res.status(500).json({ success: false, message: 'Error fetching interviews' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateRecommendation = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { interviewId, recommendation } = req.body; // recommendation: 'Recommended' | 'Not Recommended'
|
||||||
|
|
||||||
|
const interview = await Interview.findByPk(interviewId, {
|
||||||
|
include: [
|
||||||
|
{ model: InterviewParticipant, as: 'participants' },
|
||||||
|
{ model: InterviewEvaluation, as: 'evaluations' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
||||||
|
|
||||||
|
// 1. Update or Create Evaluation for Current User
|
||||||
|
let evaluation = await InterviewEvaluation.findOne({
|
||||||
|
where: { interviewId, evaluatorId: req.user?.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (evaluation) {
|
||||||
|
await evaluation.update({ recommendation });
|
||||||
|
} else {
|
||||||
|
evaluation = await InterviewEvaluation.create({
|
||||||
|
interviewId,
|
||||||
|
evaluatorId: req.user?.id,
|
||||||
|
recommendation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check for Consensus
|
||||||
|
// Refresh interview evaluations to include the one just updated/created
|
||||||
|
const updatedInterview = await Interview.findByPk(interviewId, {
|
||||||
|
include: [
|
||||||
|
{ model: InterviewParticipant, as: 'participants' },
|
||||||
|
{ model: InterviewEvaluation, as: 'evaluations' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const participants = updatedInterview?.participants || [];
|
||||||
|
const evaluations = updatedInterview?.evaluations || [];
|
||||||
|
|
||||||
|
// Filter valid panelists (exclude observers if any role logic exists, assuming all participants differ from scheduler are panelists)
|
||||||
|
const panelistIds = participants.map((p: any) => p.userId);
|
||||||
|
|
||||||
|
// Check if all panelists have evaluated with 'Selected' or equivalent positive recommendation
|
||||||
|
// Adjust logic based on exact recommendation string values used in frontend ('Selected', 'Rejected', etc.)
|
||||||
|
const allApproved = panelistIds.every((userId: string) => {
|
||||||
|
const userEval = evaluations.find((e: any) => e.evaluatorId === userId);
|
||||||
|
return userEval && (userEval.recommendation === 'Selected' || userEval.recommendation === 'Recommended');
|
||||||
|
});
|
||||||
|
|
||||||
|
const anyRejected = evaluations.some((e: any) => panelistIds.includes(e.evaluatorId) && (e.recommendation === 'Rejected' || e.recommendation === 'Not Recommended'));
|
||||||
|
|
||||||
|
if (anyRejected) {
|
||||||
|
await db.Application.update({
|
||||||
|
overallStatus: 'Rejected',
|
||||||
|
currentStage: 'Rejected'
|
||||||
|
}, { where: { id: interview.applicationId } });
|
||||||
|
|
||||||
|
await interview.update({ status: 'Completed', outcome: 'Rejected' });
|
||||||
|
|
||||||
|
} else if (allApproved) {
|
||||||
|
// Determine next status based on current level
|
||||||
|
const nextStatusMap: any = {
|
||||||
|
1: 'Level 1 Approved',
|
||||||
|
2: 'Level 2 Approved',
|
||||||
|
3: 'Level 3 Approved'
|
||||||
|
};
|
||||||
|
const newStatus = nextStatusMap[interview.level] || 'Approved';
|
||||||
|
|
||||||
|
await db.Application.update({
|
||||||
|
overallStatus: newStatus,
|
||||||
|
// Optionally update currentStage if it maps 1:1
|
||||||
|
}, { where: { id: interview.applicationId } });
|
||||||
|
|
||||||
|
await interview.update({ status: 'Completed', outcome: 'Selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Recommendation updated successfully', data: evaluation });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update recommendation error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error updating recommendation' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateInterviewDecision = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { interviewId, decision, remarks } = req.body; // decision: 'Approved' | 'Rejected'
|
||||||
|
const recommendation = decision === 'Approved' ? 'Approved' : 'Rejected';
|
||||||
|
|
||||||
|
const interview = await Interview.findByPk(interviewId);
|
||||||
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
||||||
|
|
||||||
|
// Update or Create Evaluation for the current user
|
||||||
|
let evaluation = await db.InterviewEvaluation.findOne({
|
||||||
|
where: {
|
||||||
|
interviewId,
|
||||||
|
evaluatorId: req.user?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (evaluation) {
|
||||||
|
await evaluation.update({ recommendation, decision, remarks });
|
||||||
|
} else {
|
||||||
|
evaluation = await db.InterviewEvaluation.create({
|
||||||
|
interviewId,
|
||||||
|
evaluatorId: req.user?.id,
|
||||||
|
recommendation,
|
||||||
|
decision,
|
||||||
|
remarks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always mark interview as completed when a decision is made
|
||||||
|
// This ensures action buttons hide for the user
|
||||||
|
await interview.update({ status: 'Completed' });
|
||||||
|
|
||||||
|
// Update Application Status
|
||||||
|
if (decision === 'Rejected') {
|
||||||
|
await db.Application.update({
|
||||||
|
overallStatus: 'Rejected',
|
||||||
|
currentStage: 'Rejected'
|
||||||
|
}, { where: { id: interview.applicationId } });
|
||||||
|
|
||||||
|
// Log Status History
|
||||||
|
await db.ApplicationStatusHistory.create({
|
||||||
|
applicationId: interview.applicationId,
|
||||||
|
previousStatus: 'Interview Pending',
|
||||||
|
newStatus: 'Rejected',
|
||||||
|
changedBy: req.user?.id,
|
||||||
|
reason: remarks || 'Interview Rejected'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Determine next status based on current level
|
||||||
|
const nextStatusMap: any = {
|
||||||
|
1: 'Level 1 Approved',
|
||||||
|
2: 'Level 2 Approved',
|
||||||
|
3: 'Level 3 Approved'
|
||||||
|
};
|
||||||
|
const newStatus = nextStatusMap[interview.level] || 'Approved';
|
||||||
|
|
||||||
|
// Also update currentStage for better tracking
|
||||||
|
const stageMapping: any = {
|
||||||
|
1: 'Level 1 Approved',
|
||||||
|
2: 'Level 2 Approved',
|
||||||
|
3: 'Level 3 Approved'
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.Application.update({
|
||||||
|
overallStatus: newStatus,
|
||||||
|
currentStage: stageMapping[interview.level] || newStatus
|
||||||
|
}, { where: { id: interview.applicationId } });
|
||||||
|
|
||||||
|
// Log Status History
|
||||||
|
await db.ApplicationStatusHistory.create({
|
||||||
|
applicationId: interview.applicationId,
|
||||||
|
previousStatus: 'Interview Pending',
|
||||||
|
newStatus: newStatus,
|
||||||
|
changedBy: req.user?.id,
|
||||||
|
reason: remarks || 'Interview Approved'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.AuditLog.create({
|
||||||
|
userId: req.user?.id,
|
||||||
|
action: 'UPDATED',
|
||||||
|
entityType: 'interview',
|
||||||
|
entityId: interviewId,
|
||||||
|
newData: { decision, remarks }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: `Recommendation ${decision.toLowerCase()} successfully` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update interview decision error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error updating interview decision' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -10,12 +10,14 @@ router.get('/questionnaire', assessmentController.getQuestionnaire);
|
|||||||
router.post('/questionnaire/response', assessmentController.submitQuestionnaireResponse);
|
router.post('/questionnaire/response', assessmentController.submitQuestionnaireResponse);
|
||||||
|
|
||||||
// Interviews
|
// Interviews
|
||||||
|
router.get('/interviews/:applicationId', assessmentController.getInterviews);
|
||||||
router.post('/interviews', assessmentController.scheduleInterview);
|
router.post('/interviews', assessmentController.scheduleInterview);
|
||||||
router.put('/interviews/:id', assessmentController.updateInterview);
|
router.put('/interviews/:id', assessmentController.updateInterview);
|
||||||
router.post('/interviews/:id/evaluation', assessmentController.submitEvaluation);
|
router.post('/interviews/:id/evaluation', assessmentController.submitEvaluation);
|
||||||
router.get('/interviews/:applicationId', assessmentController.getInterviews);
|
|
||||||
router.post('/kt-matrix', assessmentController.submitKTMatrix);
|
router.post('/kt-matrix', assessmentController.submitKTMatrix);
|
||||||
router.post('/level2-feedback', assessmentController.submitLevel2Feedback);
|
router.post('/level2-feedback', assessmentController.submitLevel2Feedback);
|
||||||
|
router.post('/recommendation', assessmentController.updateRecommendation);
|
||||||
|
router.post('/decision', assessmentController.updateInterviewDecision);
|
||||||
|
|
||||||
// AI Summary
|
// AI Summary
|
||||||
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);
|
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);
|
||||||
|
|||||||
@ -257,7 +257,7 @@ export const updateApplicationStatus = async (req: AuthRequest, res: Response) =
|
|||||||
export const uploadDocuments = async (req: any, res: Response) => {
|
export const uploadDocuments = async (req: any, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { documentType } = req.body;
|
const { documentType, stage } = req.body;
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@ -287,6 +287,7 @@ export const uploadDocuments = async (req: any, res: Response) => {
|
|||||||
requestId: application.id,
|
requestId: application.id,
|
||||||
requestType: 'application',
|
requestType: 'application',
|
||||||
documentType,
|
documentType,
|
||||||
|
stage: stage || null,
|
||||||
fileName: file.originalname,
|
fileName: file.originalname,
|
||||||
filePath: file.path, // Store relative path or full path as needed by your storage strategy
|
filePath: file.path, // Store relative path or full path as needed by your storage strategy
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
|||||||
20
src/scripts/test-email.ts
Normal file
20
src/scripts/test-email.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
import { sendOpportunityEmail } from '../common/utils/email.service.js';
|
||||||
|
|
||||||
|
const test = async () => {
|
||||||
|
console.log('Starting email test...');
|
||||||
|
try {
|
||||||
|
// Wait for transporter to initialize (it's async in the module)
|
||||||
|
// In a real app, the server startup time usually covers this, but for a script we might race.
|
||||||
|
// The current implementation of email.service.ts doesn't export the promise, so we might need to hack a delay.
|
||||||
|
console.log('Waiting for transporter initialization...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
await sendOpportunityEmail('test@example.com', 'Test User', 'Test City', 'APP-TEST-123');
|
||||||
|
console.log('Email send function called.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test();
|
||||||
Loading…
Reference in New Issue
Block a user