354 lines
14 KiB
TypeScript
354 lines
14 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
const { Questionnaire, QuestionnaireQuestion, QuestionnaireOption, QuestionnaireResponse, Application, ApplicationStatusHistory } = db;
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { AuthRequest } from '../../types/express.types.js';
|
|
import {
|
|
APPLICATION_STATUS,
|
|
SYSTEM_AUDIT_MODULES,
|
|
SYSTEM_AUDIT_ACTIONS
|
|
} from '../../common/config/constants.js';
|
|
import { sendQuestionnaireAckEmail } from '../../common/utils/email.service.js';
|
|
import { logSystemAudit } from '../../services/systemAuditLog.service.js';
|
|
|
|
export const getLatestQuestionnaire = async (req: Request, res: Response) => {
|
|
try {
|
|
const questionnaire = await Questionnaire.findOne({
|
|
where: { isActive: true },
|
|
include: [{
|
|
model: QuestionnaireQuestion,
|
|
as: 'questions',
|
|
order: [['order', 'ASC']],
|
|
include: [{
|
|
model: QuestionnaireOption,
|
|
as: 'questionOptions',
|
|
order: [['order', 'ASC']]
|
|
}]
|
|
}],
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
|
|
if (!questionnaire) {
|
|
return res.status(404).json({ success: false, message: 'No active questionnaire found' });
|
|
}
|
|
|
|
res.json({ success: true, data: questionnaire });
|
|
} catch (error) {
|
|
console.error('Get latest questionnaire error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
|
}
|
|
};
|
|
|
|
export const createQuestionnaireVersion = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { version, questions } = req.body; // questions is array of { text, type, options, weight, section }
|
|
|
|
// Deactivate old versions
|
|
await Questionnaire.update({ isActive: false }, { where: { isActive: true } });
|
|
|
|
const newQuestionnaire = await Questionnaire.create({
|
|
version,
|
|
isActive: true
|
|
});
|
|
|
|
if (questions && questions.length > 0) {
|
|
for (const [index, q] of questions.entries()) {
|
|
const question = await QuestionnaireQuestion.create({
|
|
questionnaireId: newQuestionnaire.id,
|
|
sectionName: q.sectionName || 'General',
|
|
questionText: q.questionText,
|
|
inputType: q.inputType || 'text',
|
|
options: null, // Legacy field
|
|
weight: q.weight || 0,
|
|
order: q.order || index + 1,
|
|
isMandatory: q.isMandatory !== false
|
|
});
|
|
|
|
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, {
|
|
include: [{ model: QuestionnaireQuestion, as: 'questions' }]
|
|
});
|
|
|
|
await logSystemAudit(req, {
|
|
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
|
|
entityType: 'questionnaire',
|
|
entityId: newQuestionnaire.id,
|
|
entityLabel: `Questionnaire ${version}`,
|
|
action: SYSTEM_AUDIT_ACTIONS.CREATED,
|
|
description: `Published questionnaire version ${version} with ${questions?.length || 0} question(s); previous active version deactivated`,
|
|
newData: {
|
|
version,
|
|
isActive: true,
|
|
questionCount: questions?.length || 0
|
|
}
|
|
});
|
|
|
|
res.status(201).json({ success: true, data: fullQuestionnaire });
|
|
} catch (error) {
|
|
console.error('Create questionnaire error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating questionnaire version' });
|
|
}
|
|
};
|
|
|
|
export const submitResponse = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId, responses } = req.body; // responses: [{ questionId, value }]
|
|
|
|
const targetId = applicationId as string;
|
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
|
|
|
|
// Verify application
|
|
const application = await Application.findOne({
|
|
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
|
});
|
|
|
|
if (!application) {
|
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
|
}
|
|
|
|
// Get active questionnaire to link
|
|
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
|
|
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
|
|
|
|
const responseRecords = responses.map((r: any) => ({
|
|
applicationId: application.id,
|
|
questionnaireId: questionnaire.id,
|
|
questionId: r.questionId,
|
|
responseValue: r.value,
|
|
attachmentUrl: r.attachmentUrl || null
|
|
}));
|
|
|
|
// Bulk create responses (maybe delete old ones for this app/questionnaire first?)
|
|
// For now, straight insert
|
|
await QuestionnaireResponse.bulkCreate(responseRecords);
|
|
|
|
// Calculate Score Logic (Placeholder for ONB-04)
|
|
// calculateAndSaveScore(applicationId, questionnaire.id);
|
|
|
|
await logSystemAudit(req, {
|
|
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
|
|
entityType: 'questionnaire_response',
|
|
entityId: application.id,
|
|
entityLabel: `Application ${application.applicationId || application.id} · ${application.applicantName || 'Applicant'}`,
|
|
action: SYSTEM_AUDIT_ACTIONS.SUBMITTED,
|
|
description: `Authenticated questionnaire response submitted (${responses.length} answer(s))`,
|
|
metadata: {
|
|
questionnaireId: questionnaire.id,
|
|
applicationId: application.id,
|
|
responseCount: responses.length
|
|
}
|
|
});
|
|
|
|
res.json({ success: true, message: 'Responses submitted successfully' });
|
|
} catch (error) {
|
|
console.error('Submit response error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
|
}
|
|
};
|
|
|
|
export const getAllQuestionnaires = async (req: Request, res: Response) => {
|
|
try {
|
|
const questionnaires = await Questionnaire.findAll({
|
|
order: [['createdAt', 'DESC']],
|
|
attributes: ['id', 'version', 'isActive', 'createdAt']
|
|
});
|
|
res.json({ success: true, data: questionnaires });
|
|
} catch (error) {
|
|
console.error('Get all questionnaires error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching questionnaires' });
|
|
}
|
|
};
|
|
|
|
export const getQuestionnaireById = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const questionnaire = await Questionnaire.findByPk(id, {
|
|
include: [{
|
|
model: QuestionnaireQuestion,
|
|
as: 'questions',
|
|
order: [['order', 'ASC']],
|
|
include: [{
|
|
model: QuestionnaireOption,
|
|
as: 'questionOptions',
|
|
order: [['order', 'ASC']]
|
|
}]
|
|
}]
|
|
});
|
|
|
|
if (!questionnaire) {
|
|
return res.status(404).json({ success: false, message: 'Questionnaire not found' });
|
|
}
|
|
|
|
res.json({ success: true, data: questionnaire });
|
|
} catch (error) {
|
|
console.error('Get questionnaire by id error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
|
}
|
|
};
|
|
|
|
export const getPublicQuestionnaire = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
|
|
// Verify valid application exists
|
|
const application = await Application.findOne({ where: { applicationId } });
|
|
if (!application) {
|
|
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
|
|
const questionnaire = await Questionnaire.findOne({
|
|
where: { isActive: true },
|
|
include: [{
|
|
model: QuestionnaireQuestion,
|
|
as: 'questions',
|
|
order: [['order', 'ASC']],
|
|
include: [{
|
|
model: QuestionnaireOption,
|
|
as: 'questionOptions',
|
|
order: [['order', 'ASC']]
|
|
}]
|
|
}]
|
|
});
|
|
|
|
if (!questionnaire) {
|
|
return res.status(404).json({ success: false, message: 'No active questionnaire found' });
|
|
}
|
|
|
|
res.json({ success: true, data: { applicationName: application.applicantName, ...questionnaire.toJSON() } });
|
|
} catch (error) {
|
|
console.error('Get public questionnaire error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
|
}
|
|
};
|
|
|
|
export const submitPublicResponse = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId, responses } = req.body;
|
|
|
|
const application = await Application.findOne({ where: { applicationId } });
|
|
if (!application) {
|
|
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 } });
|
|
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
|
|
|
|
// Calculate Score
|
|
let totalScore = 0;
|
|
const responseRecords = await Promise.all(responses.map(async (r: any) => {
|
|
const question = await QuestionnaireQuestion.findByPk(r.questionId);
|
|
let score = 0;
|
|
|
|
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);
|
|
|
|
// 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
|
|
});
|
|
|
|
try {
|
|
const location =
|
|
(application as any).preferredLocation ||
|
|
(application as any).city ||
|
|
'your preferred location';
|
|
await sendQuestionnaireAckEmail(
|
|
application.email,
|
|
application.applicantName || 'Applicant',
|
|
location,
|
|
application.applicationId || application.id
|
|
);
|
|
} catch (mailErr) {
|
|
console.error('[submitPublicResponse] acknowledgement email:', mailErr);
|
|
}
|
|
|
|
await logSystemAudit(req, {
|
|
actorName: application.applicantName || 'Public Applicant',
|
|
actorRole: 'Prospective Dealer',
|
|
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
|
|
entityType: 'questionnaire_response',
|
|
entityId: application.id,
|
|
entityLabel: `Application ${application.applicationId || application.id} · ${application.applicantName || 'Applicant'}`,
|
|
action: SYSTEM_AUDIT_ACTIONS.SUBMITTED,
|
|
description: `Public questionnaire submitted; status ${previousStatus} → ${newStatus}; total score ${totalScore}`,
|
|
oldData: { overallStatus: previousStatus },
|
|
newData: { overallStatus: newStatus, score: totalScore },
|
|
metadata: {
|
|
questionnaireId: questionnaire.id,
|
|
applicationId: application.id,
|
|
responseCount: responses.length
|
|
}
|
|
});
|
|
|
|
res.json({ success: true, message: 'Responses submitted successfully' });
|
|
} catch (error) {
|
|
console.error('Submit public response error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
|
}
|
|
};
|