questinnaire modified similar to the google form ample given with mcq and weightage . opportunity and non -opportunity mapped
This commit is contained in:
parent
859416a9a9
commit
6277a2dbc3
@ -35,6 +35,7 @@ export interface ApplicationAttributes {
|
|||||||
zoneId: string | null;
|
zoneId: string | null;
|
||||||
regionId: string | null;
|
regionId: string | null;
|
||||||
areaId: string | null;
|
areaId: string | null;
|
||||||
|
score: number;
|
||||||
documents: any[];
|
documents: any[];
|
||||||
timeline: any[];
|
timeline: any[];
|
||||||
}
|
}
|
||||||
@ -162,6 +163,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
|
score: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
assignedTo: {
|
assignedTo: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -238,6 +244,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
as: 'uploadedDocuments',
|
as: 'uploadedDocuments',
|
||||||
scope: { requestType: 'application' }
|
scope: { requestType: 'application' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return Application;
|
return Application;
|
||||||
|
|||||||
50
src/database/models/QuestionnaireOption.ts
Normal file
50
src/database/models/QuestionnaireOption.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
export interface QuestionnaireOptionAttributes {
|
||||||
|
id: string;
|
||||||
|
questionId: string;
|
||||||
|
optionText: string;
|
||||||
|
score: number;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionnaireOptionInstance extends Model<QuestionnaireOptionAttributes>, QuestionnaireOptionAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const QuestionnaireOption = sequelize.define<QuestionnaireOptionInstance>('QuestionnaireOption', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
questionId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'questionnaire_questions',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionText: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'questionnaire_options',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
(QuestionnaireOption as any).associate = (models: any) => {
|
||||||
|
QuestionnaireOption.belongsTo(models.QuestionnaireQuestion, { foreignKey: 'questionId', as: 'question' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return QuestionnaireOption;
|
||||||
|
};
|
||||||
@ -68,6 +68,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
(QuestionnaireQuestion as any).associate = (models: any) => {
|
(QuestionnaireQuestion as any).associate = (models: any) => {
|
||||||
QuestionnaireQuestion.belongsTo(models.Questionnaire, { foreignKey: 'questionnaireId', as: 'questionnaire' });
|
QuestionnaireQuestion.belongsTo(models.Questionnaire, { foreignKey: 'questionnaireId', as: 'questionnaire' });
|
||||||
QuestionnaireQuestion.hasMany(models.QuestionnaireResponse, { foreignKey: 'questionId', as: 'responses' });
|
QuestionnaireQuestion.hasMany(models.QuestionnaireResponse, { foreignKey: 'questionId', as: 'responses' });
|
||||||
|
QuestionnaireQuestion.hasMany(models.QuestionnaireOption, { foreignKey: 'questionId', as: 'questionOptions' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return QuestionnaireQuestion;
|
return QuestionnaireQuestion;
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import createApplicationProgress from './ApplicationProgress.js';
|
|||||||
// Batch 3: Questionnaire & Interview Systems
|
// Batch 3: Questionnaire & Interview Systems
|
||||||
import createQuestionnaire from './Questionnaire.js';
|
import createQuestionnaire from './Questionnaire.js';
|
||||||
import createQuestionnaireQuestion from './QuestionnaireQuestion.js';
|
import createQuestionnaireQuestion from './QuestionnaireQuestion.js';
|
||||||
|
import createQuestionnaireOption from './QuestionnaireOption.js';
|
||||||
import createQuestionnaireResponse from './QuestionnaireResponse.js';
|
import createQuestionnaireResponse from './QuestionnaireResponse.js';
|
||||||
import createQuestionnaireScore from './QuestionnaireScore.js';
|
import createQuestionnaireScore from './QuestionnaireScore.js';
|
||||||
import createInterview from './Interview.js';
|
import createInterview from './Interview.js';
|
||||||
@ -147,6 +148,7 @@ db.ApplicationProgress = createApplicationProgress(sequelize);
|
|||||||
// Batch 3: Questionnaire & Interview Systems
|
// Batch 3: Questionnaire & Interview Systems
|
||||||
db.Questionnaire = createQuestionnaire(sequelize);
|
db.Questionnaire = createQuestionnaire(sequelize);
|
||||||
db.QuestionnaireQuestion = createQuestionnaireQuestion(sequelize);
|
db.QuestionnaireQuestion = createQuestionnaireQuestion(sequelize);
|
||||||
|
db.QuestionnaireOption = createQuestionnaireOption(sequelize);
|
||||||
db.QuestionnaireResponse = createQuestionnaireResponse(sequelize);
|
db.QuestionnaireResponse = createQuestionnaireResponse(sequelize);
|
||||||
db.QuestionnaireScore = createQuestionnaireScore(sequelize);
|
db.QuestionnaireScore = createQuestionnaireScore(sequelize);
|
||||||
db.Interview = createInterview(sequelize);
|
db.Interview = createInterview(sequelize);
|
||||||
|
|||||||
@ -172,7 +172,18 @@ export const getApplicationById = async (req: Request, res: Response) => {
|
|||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{ model: ApplicationStatusHistory, as: 'statusHistory' },
|
{ model: ApplicationStatusHistory, as: 'statusHistory' },
|
||||||
{ model: ApplicationProgress, as: 'progressTracking' }
|
{ model: ApplicationProgress, as: 'progressTracking' },
|
||||||
|
{
|
||||||
|
model: db.QuestionnaireResponse,
|
||||||
|
as: 'questionnaireResponses',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.QuestionnaireQuestion,
|
||||||
|
as: 'question',
|
||||||
|
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,3 +248,65 @@ export const uploadDocuments = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error' });
|
res.status(500).json({ success: false, message: 'Error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const bulkShortlist = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { applicationIds, assignedTo, remarks } = req.body;
|
||||||
|
|
||||||
|
if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) {
|
||||||
|
return res.status(400).json({ success: false, message: 'No applications selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignedTo is expected to be an array of User IDs from frontend now
|
||||||
|
// But database only supports single UUID for assignedTo.
|
||||||
|
// Strategy: Assign the first user as primary assignee.
|
||||||
|
const primaryAssigneeId = Array.isArray(assignedTo) && assignedTo.length > 0 ? assignedTo[0] : null;
|
||||||
|
|
||||||
|
// Verify primaryAssigneeId is a valid UUID if strictly enforced by DB, but Sequelize might handle null.
|
||||||
|
|
||||||
|
// Update Applications
|
||||||
|
const updateData: any = {
|
||||||
|
ddLeadShortlisted: true,
|
||||||
|
overallStatus: 'Shortlisted',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (primaryAssigneeId) {
|
||||||
|
updateData.assignedTo = primaryAssigneeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Application.update(updateData, {
|
||||||
|
where: {
|
||||||
|
id: { [Op.in]: applicationIds }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Status History Entries
|
||||||
|
const historyEntries = applicationIds.map(appId => ({
|
||||||
|
applicationId: appId,
|
||||||
|
previousStatus: 'Questionnaire Completed',
|
||||||
|
newStatus: 'Shortlisted',
|
||||||
|
changedBy: req.user?.id,
|
||||||
|
reason: remarks ? `${remarks} (Assignees: ${Array.isArray(assignedTo) ? assignedTo.join(', ') : assignedTo})` : 'Bulk Shortlist'
|
||||||
|
}));
|
||||||
|
await ApplicationStatusHistory.bulkCreate(historyEntries);
|
||||||
|
|
||||||
|
// Audit Log
|
||||||
|
const auditEntries = applicationIds.map(appId => ({
|
||||||
|
userId: req.user?.id,
|
||||||
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
|
entityType: 'application',
|
||||||
|
entityId: appId,
|
||||||
|
newData: { ddLeadShortlisted: true, assignedTo: primaryAssigneeId, remarks }
|
||||||
|
}));
|
||||||
|
await AuditLog.bulkCreate(auditEntries);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Successfully shortlisted ${applicationIds.length} application(s)`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Bulk shortlist error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error processing shortlist' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -11,6 +11,7 @@ router.post('/apply', onboardingController.submitApplication);
|
|||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
router.get('/applications', onboardingController.getApplications);
|
router.get('/applications', onboardingController.getApplications);
|
||||||
|
router.post('/applications/shortlist', onboardingController.bulkShortlist);
|
||||||
router.get('/applications/:id', onboardingController.getApplicationById);
|
router.get('/applications/:id', onboardingController.getApplicationById);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, Application } = db;
|
const { Questionnaire, QuestionnaireQuestion, QuestionnaireOption, QuestionnaireResponse, Application, ApplicationStatusHistory } = db;
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
|
import { APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
|
|
||||||
export const getLatestQuestionnaire = async (req: Request, res: Response) => {
|
export const getLatestQuestionnaire = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@ -11,7 +12,12 @@ export const getLatestQuestionnaire = async (req: Request, res: Response) => {
|
|||||||
include: [{
|
include: [{
|
||||||
model: QuestionnaireQuestion,
|
model: QuestionnaireQuestion,
|
||||||
as: 'questions',
|
as: 'questions',
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']],
|
||||||
|
include: [{
|
||||||
|
model: QuestionnaireOption,
|
||||||
|
as: 'questionOptions',
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
}]
|
||||||
}],
|
}],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
@ -40,18 +46,28 @@ export const createQuestionnaireVersion = async (req: AuthRequest, res: Response
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (questions && questions.length > 0) {
|
if (questions && questions.length > 0) {
|
||||||
const questionRecords = questions.map((q: any, index: number) => ({
|
for (const [index, q] of questions.entries()) {
|
||||||
questionnaireId: newQuestionnaire.id,
|
const question = await QuestionnaireQuestion.create({
|
||||||
sectionName: q.sectionName || 'General',
|
questionnaireId: newQuestionnaire.id,
|
||||||
questionText: q.questionText,
|
sectionName: q.sectionName || 'General',
|
||||||
inputType: q.inputType || 'text',
|
questionText: q.questionText,
|
||||||
options: q.options || null,
|
inputType: q.inputType || 'text',
|
||||||
weight: q.weight || 0,
|
options: null, // Legacy field
|
||||||
order: q.order || index + 1,
|
weight: q.weight || 0,
|
||||||
isMandatory: q.isMandatory !== false
|
order: q.order || index + 1,
|
||||||
}));
|
isMandatory: q.isMandatory !== false
|
||||||
|
});
|
||||||
|
|
||||||
await QuestionnaireQuestion.bulkCreate(questionRecords);
|
if (q.options && Array.isArray(q.options) && q.options.length > 0) {
|
||||||
|
const optionRecords = q.options.map((opt: any, idx: number) => ({
|
||||||
|
questionId: question.id,
|
||||||
|
optionText: opt.text,
|
||||||
|
score: opt.score || 0,
|
||||||
|
order: idx + 1
|
||||||
|
}));
|
||||||
|
await QuestionnaireOption.bulkCreate(optionRecords);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullQuestionnaire = await Questionnaire.findByPk(newQuestionnaire.id, {
|
const fullQuestionnaire = await Questionnaire.findByPk(newQuestionnaire.id, {
|
||||||
@ -121,7 +137,12 @@ export const getQuestionnaireById = async (req: Request, res: Response) => {
|
|||||||
include: [{
|
include: [{
|
||||||
model: QuestionnaireQuestion,
|
model: QuestionnaireQuestion,
|
||||||
as: 'questions',
|
as: 'questions',
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']],
|
||||||
|
include: [{
|
||||||
|
model: QuestionnaireOption,
|
||||||
|
as: 'questionOptions',
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,13 +167,27 @@ export const getPublicQuestionnaire = async (req: Request, res: Response) => {
|
|||||||
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if already submitted
|
||||||
|
if (application.overallStatus !== APPLICATION_STATUS.QUESTIONNAIRE_PENDING) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Questionnaire already submitted or link expired',
|
||||||
|
code: 'ALREADY_SUBMITTED' // Frontend can use this code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch active questionnaire
|
// Fetch active questionnaire
|
||||||
const questionnaire = await Questionnaire.findOne({
|
const questionnaire = await Questionnaire.findOne({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
include: [{
|
include: [{
|
||||||
model: QuestionnaireQuestion,
|
model: QuestionnaireQuestion,
|
||||||
as: 'questions',
|
as: 'questions',
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']],
|
||||||
|
include: [{
|
||||||
|
model: QuestionnaireOption,
|
||||||
|
as: 'questionOptions',
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,19 +211,67 @@ export const submitPublicResponse = async (req: Request, res: Response) => {
|
|||||||
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double check status before processing
|
||||||
|
if (application.overallStatus !== APPLICATION_STATUS.QUESTIONNAIRE_PENDING) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Questionnaire already submitted' });
|
||||||
|
}
|
||||||
|
|
||||||
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
|
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
|
||||||
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
|
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
|
||||||
|
|
||||||
const responseRecords = responses.map((r: any) => ({
|
// Calculate Score
|
||||||
applicationId: application.id, // Use UUID from database
|
let totalScore = 0;
|
||||||
questionnaireId: questionnaire.id,
|
const responseRecords = await Promise.all(responses.map(async (r: any) => {
|
||||||
questionId: r.questionId,
|
const question = await QuestionnaireQuestion.findByPk(r.questionId);
|
||||||
responseValue: r.value,
|
let score = 0;
|
||||||
attachmentUrl: r.attachmentUrl || null
|
|
||||||
|
if (question) {
|
||||||
|
// If question has options (via new table), find score
|
||||||
|
const option = await QuestionnaireOption.findOne({
|
||||||
|
where: {
|
||||||
|
questionId: question.id,
|
||||||
|
optionText: r.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
score = option.score || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalScore += score;
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationId: application.id, // Use UUID from database
|
||||||
|
questionnaireId: questionnaire.id,
|
||||||
|
questionId: r.questionId,
|
||||||
|
responseValue: r.value,
|
||||||
|
attachmentUrl: r.attachmentUrl || null,
|
||||||
|
score: score // Store individual answer score if needed (requires schema update for QuestionnaireResponse)
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await QuestionnaireResponse.bulkCreate(responseRecords);
|
await QuestionnaireResponse.bulkCreate(responseRecords);
|
||||||
|
|
||||||
|
// Update Application Status & Score
|
||||||
|
const previousStatus = application.overallStatus;
|
||||||
|
const newStatus = APPLICATION_STATUS.QUESTIONNAIRE_COMPLETED;
|
||||||
|
|
||||||
|
await application.update({
|
||||||
|
overallStatus: newStatus,
|
||||||
|
score: totalScore,
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log Status History
|
||||||
|
await ApplicationStatusHistory.create({
|
||||||
|
applicationId: application.id,
|
||||||
|
previousStatus,
|
||||||
|
newStatus,
|
||||||
|
reason: 'Public Questionnaire Submitted',
|
||||||
|
changedBy: null // System action / Public user
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ success: true, message: 'Responses submitted successfully' });
|
res.json({ success: true, message: 'Responses submitted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit public response error:', error);
|
console.error('Submit public response error:', error);
|
||||||
|
|||||||
387
src/scripts/seedQuestionnaire.ts
Normal file
387
src/scripts/seedQuestionnaire.ts
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
import db from '../database/models/index.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const seedQuestionnaire = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Seeding Questionnaire...');
|
||||||
|
console.log('DB Keys:', Object.keys(db));
|
||||||
|
console.log('QuestionnaireOption defined?', !!db.QuestionnaireOption);
|
||||||
|
|
||||||
|
// Ensure database schema is up to date
|
||||||
|
console.log('Syncing database...');
|
||||||
|
await db.sequelize.sync({ alter: true });
|
||||||
|
|
||||||
|
// Deactivate existing questionnaires
|
||||||
|
await db.Questionnaire.update({ isActive: false }, { where: {} });
|
||||||
|
|
||||||
|
// Create new questionnaire
|
||||||
|
const questionnaire = await db.Questionnaire.create({
|
||||||
|
id: uuidv4(),
|
||||||
|
version: 'v1.0',
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Created Questionnaire: ${questionnaire.id}`);
|
||||||
|
|
||||||
|
const questions = [
|
||||||
|
// Section 1: Basic Information (0 Score)
|
||||||
|
{
|
||||||
|
text: "Email",
|
||||||
|
type: "email",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Name",
|
||||||
|
type: "text",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Current Location",
|
||||||
|
type: "text",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Location Applied For",
|
||||||
|
type: "text",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "State (Applied for)",
|
||||||
|
type: "select",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: [
|
||||||
|
{ text: "Andaman & Nicobar", score: 0 },
|
||||||
|
{ text: "Andhra Pradesh", score: 0 },
|
||||||
|
{ text: "Arunachal Pradesh", score: 0 },
|
||||||
|
{ text: "Assam", score: 0 },
|
||||||
|
{ text: "Bihar", score: 0 },
|
||||||
|
{ text: "Chandigarh", score: 0 },
|
||||||
|
{ text: "Chhattisgarh", score: 0 },
|
||||||
|
{ text: "Delhi & NCR", score: 0 },
|
||||||
|
{ text: "Goa", score: 0 },
|
||||||
|
{ text: "Gujarat", score: 0 },
|
||||||
|
{ text: "Himachal Pradesh", score: 0 },
|
||||||
|
{ text: "Haryana", score: 0 },
|
||||||
|
{ text: "Jammu & Kashmir", score: 0 },
|
||||||
|
{ text: "Jharkhand", score: 0 },
|
||||||
|
{ text: "Karnataka", score: 0 },
|
||||||
|
{ text: "Kerala", score: 0 },
|
||||||
|
{ text: "Ladakh", score: 0 },
|
||||||
|
{ text: "Madhya Pradesh", score: 0 },
|
||||||
|
{ text: "Maharashtra", score: 0 },
|
||||||
|
{ text: "Mizoram", score: 0 },
|
||||||
|
{ text: "Meghalaya", score: 0 },
|
||||||
|
{ text: "Manipur", score: 0 },
|
||||||
|
{ text: "Nagaland", score: 0 },
|
||||||
|
{ text: "Odisha", score: 0 },
|
||||||
|
{ text: "Puducherry", score: 0 },
|
||||||
|
{ text: "Punjab", score: 0 },
|
||||||
|
{ text: "Rajasthan", score: 0 },
|
||||||
|
{ text: "Sikkim", score: 0 },
|
||||||
|
{ text: "Tamilnadu", score: 0 },
|
||||||
|
{ text: "Telangana", score: 0 },
|
||||||
|
{ text: "Tripura", score: 0 },
|
||||||
|
{ text: "Uttar Pradesh", score: 0 },
|
||||||
|
{ text: "Uttarakhand", score: 0 },
|
||||||
|
{ text: "West Bengal", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 0,
|
||||||
|
order: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Contact Number",
|
||||||
|
type: "text",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Age",
|
||||||
|
type: "number",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 7
|
||||||
|
},
|
||||||
|
|
||||||
|
// Section 2: Profile & Background (Scoring Starts)
|
||||||
|
{
|
||||||
|
text: "Educational Qualification",
|
||||||
|
type: "radio",
|
||||||
|
section: "Profile & Background",
|
||||||
|
options: [
|
||||||
|
{ text: "Under Graduate", score: 5 },
|
||||||
|
{ text: "Graduate", score: 10 },
|
||||||
|
{ text: "Post Graduate", score: 15 }
|
||||||
|
],
|
||||||
|
weight: 15, // Max possible
|
||||||
|
order: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "What is your Personal Networth",
|
||||||
|
type: "select",
|
||||||
|
section: "Financials",
|
||||||
|
options: [
|
||||||
|
{ text: "Less than 2 Crores", score: 5 },
|
||||||
|
{ text: "Between 2 - 5 Crores", score: 10 },
|
||||||
|
{ text: "Between 5 - 10 Crores", score: 15 },
|
||||||
|
{ text: "Between 10 - 15 Crores", score: 20 },
|
||||||
|
{ text: "Greater than 15 Crores", score: 25 }
|
||||||
|
],
|
||||||
|
weight: 25,
|
||||||
|
order: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Are you a native of the Proposed Location?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Location",
|
||||||
|
options: [
|
||||||
|
{ text: "Native", score: 10 },
|
||||||
|
{ text: "Willing to Relocate", score: 5 },
|
||||||
|
{ text: "Will manage Remotely", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Proposed Location Photos (If any)",
|
||||||
|
type: "file",
|
||||||
|
section: "Location",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Why do you want to partner with Royal Enfield?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Strategy",
|
||||||
|
options: [
|
||||||
|
{ text: "Absence of Royal Enfield in the particular location and presence of opportunity", score: 5 },
|
||||||
|
{ text: "Passionate about the brand", score: 5 },
|
||||||
|
{ text: "Experience in the automobile business and would like to expand with Royal Enfield", score: 10 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Who will be the partners in proposed company?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Business Structure",
|
||||||
|
options: [
|
||||||
|
{ text: "Immediate Family", score: 5 },
|
||||||
|
{ text: "Extended Family", score: 3 },
|
||||||
|
{ text: "Friends", score: 2 },
|
||||||
|
{ text: "Proprietorship", score: 5 }
|
||||||
|
],
|
||||||
|
weight: 5,
|
||||||
|
order: 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Who will be managing the Royal Enfield dealership",
|
||||||
|
type: "radio",
|
||||||
|
section: "Business Structure",
|
||||||
|
options: [
|
||||||
|
{ text: "I will be managing full time", score: 10 },
|
||||||
|
{ text: "I will be managing with my partners", score: 7 },
|
||||||
|
{ text: "I will hire a manager full time and oversee the operations", score: 5 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Proposed Firm Type",
|
||||||
|
type: "radio",
|
||||||
|
section: "Business Structure",
|
||||||
|
options: [
|
||||||
|
{ text: "Proprietorship", score: 5 },
|
||||||
|
{ text: "Partnership", score: 5 },
|
||||||
|
{ text: "Limited Liability partnership", score: 5 },
|
||||||
|
{ text: "Private Limited Company", score: 10 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "What are you currently doing?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Experience",
|
||||||
|
options: [
|
||||||
|
{ text: "Running automobile dealership", score: 10 },
|
||||||
|
{ text: "Running another business not in automobile field", score: 5 },
|
||||||
|
{ text: "Presently working, willing to resign and handle business", score: 5 },
|
||||||
|
{ text: "Currently not working and looking for business opportunities", score: 2 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Do you own a property in proposed location?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Location",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes", score: 10 },
|
||||||
|
{ text: "No, will rent a location in the desired location", score: 5 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "How are you planning to invest in the Royal Enfield business",
|
||||||
|
type: "radio",
|
||||||
|
section: "Financials",
|
||||||
|
options: [
|
||||||
|
{ text: "I will be investing my own funds", score: 10 },
|
||||||
|
{ text: "I will invest partially and get the rest from the bank", score: 7 },
|
||||||
|
{ text: "I will be requiring complete funds from the bank", score: 3 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "What are your plans of expansion with RE?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Strategy",
|
||||||
|
options: [
|
||||||
|
{ text: "Willing to expand with the help of partners", score: 5 },
|
||||||
|
{ text: "Willing to expand by myself", score: 10 },
|
||||||
|
{ text: "No plans for expansion", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 10,
|
||||||
|
order: 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Will you be expanding to any other automobile OEM in the future?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Strategy",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes", score: 0 },
|
||||||
|
{ text: "No", score: 5 }
|
||||||
|
],
|
||||||
|
weight: 5,
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Do you own a Royal Enfield ?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Brand Loyalty",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes, it is registered in my name", score: 5 },
|
||||||
|
{ text: "Yes, it is registered to my immediate family member", score: 3 },
|
||||||
|
{ text: "Not at the moment but owned it earlier", score: 2 },
|
||||||
|
{ text: "No", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 5,
|
||||||
|
order: 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Do you go for long leisure rides",
|
||||||
|
type: "radio",
|
||||||
|
section: "Brand Loyalty",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes, with the Royal Enfield riders", score: 5 },
|
||||||
|
{ text: "Yes, with other brands", score: 3 },
|
||||||
|
{ text: "No", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 5,
|
||||||
|
order: 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "What special initiatives do you plan to implement if selected as business partner for Royal Enfield ?",
|
||||||
|
type: "textarea",
|
||||||
|
section: "Strategy",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Please elaborate your present business/employment.",
|
||||||
|
type: "textarea",
|
||||||
|
section: "Experience",
|
||||||
|
options: null,
|
||||||
|
weight: 0,
|
||||||
|
order: 24
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const q of questions) {
|
||||||
|
const questionId = uuidv4();
|
||||||
|
await db.QuestionnaireQuestion.create({
|
||||||
|
id: questionId,
|
||||||
|
questionnaireId: questionnaire.id,
|
||||||
|
sectionName: q.section,
|
||||||
|
questionText: q.text,
|
||||||
|
inputType: q.type,
|
||||||
|
options: null, // Legacy field
|
||||||
|
isMandatory: true,
|
||||||
|
weight: q.weight,
|
||||||
|
order: q.order
|
||||||
|
});
|
||||||
|
|
||||||
|
// If question has options, seed them into QuestionnaireOption table
|
||||||
|
if (q.options && Array.isArray(q.options) && q.options.length > 0) {
|
||||||
|
console.log(`Seeding options for question: ${q.text}`);
|
||||||
|
|
||||||
|
if (!db.QuestionnaireOption) {
|
||||||
|
console.error('CRITICAL: db.QuestionnaireOption is undefined!');
|
||||||
|
console.log('Available models:', Object.keys(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionRecords = q.options.map((opt, idx) => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
questionId: questionId,
|
||||||
|
optionText: opt.text,
|
||||||
|
score: opt.score,
|
||||||
|
order: idx + 1
|
||||||
|
}));
|
||||||
|
const OptionModel = db.sequelize.models.QuestionnaireOption || db.QuestionnaireOption;
|
||||||
|
|
||||||
|
console.log(`[DEBUG] Question: "${q.text}"`);
|
||||||
|
console.log(`[DEBUG] OptionModel type: ${typeof OptionModel}`);
|
||||||
|
console.log(`[DEBUG] OptionModel keys: ${OptionModel ? Object.keys(OptionModel).slice(0, 5) : 'null'}`);
|
||||||
|
console.log(`[DEBUG] bulkCreate type: ${OptionModel ? typeof OptionModel.bulkCreate : 'undefined'}`);
|
||||||
|
|
||||||
|
if (!OptionModel) {
|
||||||
|
throw new Error(`QuestionnaireOption model not found in sequelize.models or db object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await OptionModel.bulkCreate(optionRecords);
|
||||||
|
console.log(`[DEBUG] Success seeding options for "${q.text}"`);
|
||||||
|
} catch (innerErr: any) {
|
||||||
|
console.error(`[DEBUG] Failed to seed options for "${q.text}"`);
|
||||||
|
console.error('Error Name:', innerErr.name);
|
||||||
|
console.error('Error Message:', innerErr.message);
|
||||||
|
if (innerErr.parent) {
|
||||||
|
console.error('Parent Error:', innerErr.parent);
|
||||||
|
}
|
||||||
|
if (innerErr.original) {
|
||||||
|
console.error('Original Error:', innerErr.original);
|
||||||
|
}
|
||||||
|
throw innerErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Seeded questions and options successfully.`);
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding questionnaire:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
seedQuestionnaire();
|
||||||
109
src/scripts/verifyScoring.ts
Normal file
109
src/scripts/verifyScoring.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import db from '../database/models/index.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const verifyScoring = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Verifying Questionnaire Scoring...');
|
||||||
|
|
||||||
|
// Sync DB to ensure 'score' column exists
|
||||||
|
await db.sequelize.sync({ alter: true });
|
||||||
|
|
||||||
|
// 1. Create a dummy test application
|
||||||
|
const appId = uuidv4();
|
||||||
|
const testApp = await db.Application.create({
|
||||||
|
applicationId: `TEST-${Date.now()}`,
|
||||||
|
applicantName: 'Test Scorer',
|
||||||
|
email: 'test.scorer@example.com',
|
||||||
|
phone: '1234567890',
|
||||||
|
businessType: 'Dealership',
|
||||||
|
currentStage: 'DD',
|
||||||
|
overallStatus: 'Questionnaire Pending',
|
||||||
|
score: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Created Test Application: ${testApp.applicationId} (${testApp.id})`);
|
||||||
|
|
||||||
|
// 2. Fetch Active Questionnaire Questions
|
||||||
|
const questionnaire = await db.Questionnaire.findOne({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: [{ model: db.QuestionnaireQuestion, as: 'questions' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!questionnaire) throw new Error('No active questionnaire found');
|
||||||
|
|
||||||
|
// 3. Construct Responses to get a specific score
|
||||||
|
// We will target:
|
||||||
|
// - "Educational Qualification": "Post Graduate" (Score: 15)
|
||||||
|
// - "Do you own a property...": "Yes" (Score: 10)
|
||||||
|
// - All others: Non-scoring or first option (usually > 0)
|
||||||
|
|
||||||
|
const responses = questionnaire.questions.map((q: any) => {
|
||||||
|
let value = "Test Answer"; // Default for text/file
|
||||||
|
|
||||||
|
if (q.questionText === "Educational Qualification") {
|
||||||
|
value = "Post Graduate";
|
||||||
|
} else if (q.questionText === "Do you own a property in proposed location?") {
|
||||||
|
value = "Yes";
|
||||||
|
} else if (q.inputType === 'select' || q.inputType === 'radio') {
|
||||||
|
// Pick first option if not targeting specific score
|
||||||
|
const opts = typeof q.options === 'string' ? JSON.parse(q.options) : q.options;
|
||||||
|
if (opts && opts.length > 0) {
|
||||||
|
value = opts[0].text || opts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionId: q.id,
|
||||||
|
value: value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Submitting responses...');
|
||||||
|
|
||||||
|
// 4. Submit to API (assuming local server running on 5000)
|
||||||
|
try {
|
||||||
|
const res = await fetch('http://localhost:5000/api/questionnaire/public/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
applicationId: testApp.applicationId,
|
||||||
|
responses: responses
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data: any = await res.json();
|
||||||
|
console.log('API Response:', data);
|
||||||
|
} catch (apiError: any) {
|
||||||
|
console.error('API Error:', apiError.message);
|
||||||
|
fs.writeFileSync('verification_result.txt', `FAILURE: API Error - ${apiError.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check Score in DB
|
||||||
|
const updatedApp = await db.Application.findByPk(testApp.id);
|
||||||
|
console.log(`New Score: ${updatedApp.score}`);
|
||||||
|
console.log(`New Status: ${updatedApp.overallStatus}`);
|
||||||
|
|
||||||
|
// Write result to file
|
||||||
|
if (updatedApp.score >= 25) {
|
||||||
|
console.log('SUCCESS: Score updated!');
|
||||||
|
fs.writeFileSync('verification_result.txt', `SUCCESS: Score ${updatedApp.score} matched expectation (>= 25)`);
|
||||||
|
} else {
|
||||||
|
console.error('FAILURE: Score mismatch.');
|
||||||
|
fs.writeFileSync('verification_result.txt', `FAILURE: Score ${updatedApp.score} mismatch (expected >= 25)`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await testApp.destroy();
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Verification failed:', error);
|
||||||
|
fs.writeFileSync('verification_result.txt', `FAILURE: Script Error - ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// await db.sequelize.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
verifyScoring();
|
||||||
1
verification_result.txt
Normal file
1
verification_result.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
FAILURE: Script Error - update or delete on table "applications" violates foreign key constraint "questionnaire_responses_applicationId_fkey" on table "questionnaire_responses"
|
||||||
Loading…
Reference in New Issue
Block a user