diff --git a/docs/dealer_onboard_backend_schema.mermaid b/docs/dealer_onboard_backend_schema.mermaid index 7efe3d4..5c14cc6 100644 --- a/docs/dealer_onboard_backend_schema.mermaid +++ b/docs/dealer_onboard_backend_schema.mermaid @@ -247,11 +247,27 @@ erDiagram uuid area_id FK uuid assigned_dd_zm FK uuid assigned_rbm FK + json documents + json timeline + integer progress_percentage timestamp submitted_at timestamp created_at timestamp updated_at } + APPLICATION_PROGRESS { + uuid id PK + uuid application_id FK + string stage_name + integer stage_order + string status + integer completion_percentage + timestamp stage_started_at + timestamp stage_completed_at + timestamp created_at + timestamp updated_at + } + %% ============================================ %% QUESTIONNAIRE MANAGEMENT %% ============================================ diff --git a/docs/project_status_report.md b/docs/project_status_report.md new file mode 100644 index 0000000..fb44b47 --- /dev/null +++ b/docs/project_status_report.md @@ -0,0 +1,95 @@ +# Project Status & Timeline Report (Strict Verification) + +## Executive Summary +This report outlines the current implementation status of the Dealer Onboarding System based on a strict audit of both Frontend and Backend codebases. + +**Criteria:** +- **Done:** Fully implemented in both Frontend (UI/Routes) and Backend (Controllers/DB). +- **Partial:** Frontend exists but Backend is missing or incomplete (or vice versa). +- **Pending:** Not yet implemented. + +**Overall Readiness:** ~65% Fully Integrated +**Target Completion:** February 28, 2026 + +--- + +## Detailed Module Status + +### 1. Onboarding Module (ONB) +**Status:** **High Readiness** (Frontend & Backend Aligned) + +| ID | Task Name | Status | Backend Verification | Notes | +|:---|:---|:---|:---|:---| +| ONB-01 | Dealership Application Form | **Done** | `onboarding.controller.ts` | Validated Public API | +| ONB-02 | Opportunity / Non-Opportunity | **Done** | `opportunity` module | Master data & selection flow active | +| ONB-03 | Questionnaire Master | **Done** | `assessment` module | Builder UI & Schema ready | +| ONB-04 | Questionnaire Response & Scoring | **Done** | `assessment` module | Scoring logic active | +| ONB-05 | Shortlisting Process | **Done** | `application` status | Logic for DD/Lead shortlisting active | +| ONB-06 | Shortlisted Applications | **Done** | `application` filters | Filtered views available | +| ONB-07 | Application Detail View | **Done** | `ApplicationProgress` model | Granular timeline mapping complete | +| ONB-08 | Interview Scheduling | **Done** | `assessment.controller.ts` | Scheduling endpoints exist | +| ONB-09 | Interview Evaluation (KT Matrix) | **Done** | `assessment` scoring | Scoring models & forms ready | +| ONB-10 | AI Interview Summary | **Pending** | *Missing* | Schema fits, AI logic pending | +| ONB-11 | Interview Summary | **Done** | Manual entry | Manual summary view active | +| ONB-12 | FDD Assignment | **Done** | `fdd` module | Assignment logic functional | +| ONB-13 | LOI Approval & Issuance | **Done** | `loi` module | Workflow active | +| ONB-14 | Dealer Code Creation | **Done** | `dealer` module | Generates codes on approval | +| ONB-15 | Architecture & Statutory Docs | **Done** | `documents` API | Detailed step-tracking active | +| ONB-16 | LOA Issuance | **Done** | `loa` module | Triggered after EOR | +| ONB-17 | EOR Checklist | **Done** | `eor` module | Checklist UI & validation active | +| ONB-18 | Inauguration & Go-Live | **Done** | `dealer` module | Final stage tracking ready | + +### 2. Resignation Module (RES) +**Status:** **Good Readiness** (Solid Backend Logic) + +| ID | Task Name | Status | Backend Verification | Notes | +|:---|:---|:---|:---|:---| +| RES-01 | Resignation Initiation | **Done** | `resignation.controller.ts` | Full creation logic present | +| RES-02 | ASM Review | **Done** | `approveResignation` | Workflow step active | +| RES-03 | RBM + DD-ZM Review | **Done** | `approveResignation` | Parallel approval logic ready | +| RES-04 | ZBH Review | **Done** | `approveResignation` | Escalation logic ready | +| RES-05 | DD-Lead Review | **Done** | `approveResignation` | Review screen active | +| RES-06 | NBH Approval | **Done** | `approveResignation` | Final approval step ready | +| RES-07 | Legal Acceptance Letter | **Partial** | Logic Missing | Template ready, PDF generation missing | +| RES-08 | Closure & F&F Trigger | **Done** | `resignation.controller.ts` | Auto-trigger to Finance module ready | + +### 3. Termination Module (TER) +**Status:** **Partial (Frontend Only)** +*Critical Gap:* Backend `termination` folder is empty. + +| ID | Task Name | Status | Backend Verification | Notes | +|:---|:---|:---|:---|:---| +| TER-01 | Termination Initiation | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-02 | RBM + DD-ZM Review | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-03 | ZBH Review | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-04 | DD-Lead & Legal Review | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-05 | DD-Head & NBH Review | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-06 | CEO & CCO Approval | **Partial** | *Missing* | Frontend UI ready, Backend missing | +| TER-07 | Termination Letter | **Pending** | *Missing* | Not implemented | +| TER-08 | Closure & F&F Trigger | **Pending** | *Missing* | Not implemented | + +### 4. Finance (F&F) Module (FF) +**Status:** **Basic Implementation** + +| ID | Task Name | Status | Backend Verification | Notes | +|:---|:---|:---|:---|:---| +| FF-01 | F&F Case Initiation | **Done** | `settlement.controller.ts` | Triggered by RES module | +| FF-02 | Dept-wise Clearance | **Partial** | *Simplified* | `settlement` controller lacks granular NOC logic | +| FF-03 | Finance Summary | **Done** | `updateFnF` | Calculation logic ready | +| FF-04 | Dealer Acknowledgement | **Partial** | Logic Missing | Dealer portal side pending | +| FF-05 | Final Finance Approval | **Done** | `updatePayment` | Payment release workflow ready | +| FF-06 | F&F Closure | **Done** | `updateFnF` | Archival logic ready | + +### 5. Admin Module (ADM) +**Status:** **High Readiness** + +| ID | Task Name | Status | Backend Verification | Notes | +|:---|:---|:---|:---|:---| +| ADM-01 | Role & Permission Mgmt | **Done** | `admin.controller.ts` | RBAC fully active | +| ADM-02 | Org / Zone / Region Master | **Done** | `master.controller.ts` | Master data pages active | +| ADM-03 | SLA & Escalation | **Done** | `sla` module | Configurable timers ready | +| ADM-04 | Templates Management | **Partial** | *Missing* | UI ready, backend variable mapping pending | +| ADM-05 | Opportunity Master | **Done** | `opportunity` module | CRUD operations active | + +--- +*Status checked against codebase on Jan 28, 2026.* diff --git a/scripts/debug-area-manager.ts b/scripts/debug-area-manager.ts new file mode 100644 index 0000000..5651666 --- /dev/null +++ b/scripts/debug-area-manager.ts @@ -0,0 +1,63 @@ + +import db from '../src/database/models/index.js'; + +async function checkAreaManager() { + try { + console.log('Connecting to database...'); + await db.sequelize.authenticate(); + console.log('Database connected.'); + + // Fetch all areas + const areas = await db.Area.findAll({ + include: [ + { model: db.User, as: 'manager', attributes: ['id', 'fullName'] } + ] + }); + + console.log(`Found ${areas.length} areas.`); + + if (areas.length > 0) { + areas.forEach((area: any) => { + console.log(`Area: ${area.areaName} (${area.id})`); + console.log(` - Manager ID (Field): ${area.managerId}`); + console.log(` - Manager (Association): ${area.manager ? area.manager.fullName : 'None'}`); + console.log('-----------------------------------'); + }); + + // Pick the first area and try to update it manually if managerId is null + const targetArea = areas[0]; + // Find a user to assign (any user) + const user = await db.User.findOne(); + + if (user) { + console.log(`Attempting to assign User ${user.fullName} (${user.id}) to Area ${targetArea.areaName}...`); + + targetArea.managerId = user.id; + await targetArea.save(); + + console.log('Update saved. Re-fetching to verify...'); + + const updatedArea = await db.Area.findByPk(targetArea.id); + console.log(`Re-fetched Area Manager ID: ${updatedArea?.managerId}`); + + if (updatedArea?.managerId === user.id) { + console.log('SUCCESS: Manager ID persisted correctly.'); + } else { + console.error('FAILURE: Manager ID did not persist.'); + } + } else { + console.log('No users found to test assignment.'); + } + + } else { + console.log('No areas found.'); + } + + } catch (error) { + console.error('Error:', error); + } finally { + await db.sequelize.close(); + } +} + +checkAreaManager(); diff --git a/scripts/fix-asm-column.ts b/scripts/fix-asm-column.ts new file mode 100644 index 0000000..ec0beb2 --- /dev/null +++ b/scripts/fix-asm-column.ts @@ -0,0 +1,26 @@ + +import { Sequelize } from 'sequelize'; + +const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', 'Admin@123', { + host: 'localhost', + dialect: 'postgres', + logging: console.log +}); + +const run = async () => { + try { + await sequelize.authenticate(); + console.log('Connected to database.'); + + console.log('Adding asmCode column to area_managers table...'); + await sequelize.query('ALTER TABLE "area_managers" ADD COLUMN IF NOT EXISTS "asmCode" VARCHAR(255);'); + + console.log('Column added successfully.'); + process.exit(0); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +}; + +run(); diff --git a/scripts/force-sync.ts b/scripts/force-sync.ts new file mode 100644 index 0000000..bbfc2e2 --- /dev/null +++ b/scripts/force-sync.ts @@ -0,0 +1,21 @@ + +import db from '../src/database/models/index.ts'; + +const syncDb = async () => { + try { + console.log('Connecting to database...'); + await db.sequelize.authenticate(); + console.log('Database connected.'); + + console.log('Syncing database schema (alter: true)...'); + await db.sequelize.sync({ alter: true }); + console.log('Database synced successfully.'); + + process.exit(0); + } catch (error) { + console.error('Error syncing database:', error); + process.exit(1); + } +}; + +syncDb(); diff --git a/scripts/test-areas.ts b/scripts/test-areas.ts new file mode 100644 index 0000000..31aeeed --- /dev/null +++ b/scripts/test-areas.ts @@ -0,0 +1,21 @@ + +import db from '../src/database/models/index.js'; +const { Area, District, User } = db; + +async function testAreas() { + try { + console.log('Testing Area.findAll...'); + const areas = await Area.findAll({ + include: [ + { model: District, as: 'district', attributes: ['districtName'] }, + { model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] } + ], + order: [['areaName', 'ASC']] + }); + console.log('Successfully fetched areas:', JSON.stringify(areas, null, 2)); + } catch (error) { + console.error('Error fetching areas:', error); + } +} + +testAreas(); diff --git a/scripts/test-regions.ts b/scripts/test-regions.ts new file mode 100644 index 0000000..612d4db --- /dev/null +++ b/scripts/test-regions.ts @@ -0,0 +1,34 @@ + +import db from '../src/database/models/index.js'; +const { Region, Zone, State, User } = db; + +async function testRegions() { + try { + console.log('Testing Region.findAll...'); + const regions = await Region.findAll({ + include: [ + { + model: State, + as: 'states', + attributes: ['id', 'stateName'] + }, + { + model: Zone, + as: 'zone', + attributes: ['id', 'zoneName'] + }, + { + model: User, + as: 'regionalManager', + attributes: ['id', 'fullName', 'email', 'mobileNumber'] + } + ], + order: [['regionName', 'ASC']] + }); + console.log('Successfully fetched regions:', JSON.stringify(regions, null, 2)); + } catch (error) { + console.error('Error fetching regions:', error); + } +} + +testRegions(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 6dc62d5..1bd600d 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -44,7 +44,37 @@ export const APPLICATION_STATUS = { PENDING: 'Pending', IN_REVIEW: 'In Review', APPROVED: 'Approved', - REJECTED: 'Rejected' + REJECTED: 'Rejected', + SUBMITTED: 'Submitted', + QUESTIONNAIRE_PENDING: 'Questionnaire Pending', + LEVEL_1_PENDING: 'Level 1 Pending', + LEVEL_1_APPROVED: 'Level 1 Approved', + LEVEL_2_PENDING: 'Level 2 Pending', + LEVEL_2_APPROVED: 'Level 2 Approved', + LEVEL_2_RECOMMENDED: 'Level 2 Recommended', + LEVEL_3_PENDING: 'Level 3 Pending', + FDD_VERIFICATION: 'FDD Verification', + PAYMENT_PENDING: 'Payment Pending', + LOI_ISSUED: 'LOI Issued', + DEALER_CODE_GENERATION: 'Dealer Code Generation', + ARCHITECTURE_TEAM_ASSIGNED: 'Architecture Team Assigned', + ARCHITECTURE_DOCUMENT_UPLOAD: 'Architecture Document Upload', + ARCHITECTURE_TEAM_COMPLETION: 'Architecture Team Completion', + STATUTORY_GST: 'Statutory GST', + STATUTORY_PAN: 'Statutory PAN', + STATUTORY_NODAL: 'Statutory Nodal', + STATUTORY_CHECK: 'Statutory Check', + STATUTORY_PARTNERSHIP: 'Statutory Partnership', + STATUTORY_FIRM_REG: 'Statutory Firm Reg', + STATUTORY_VIRTUAL_CODE: 'Statutory Virtual Code', + STATUTORY_DOMAIN: 'Statutory Domain', + STATUTORY_MSD: 'Statutory MSD', + STATUTORY_LOI_ACK: 'Statutory LOI Ack', + EOR_IN_PROGRESS: 'EOR In Progress', + LOA_PENDING: 'LOA Pending', + EOR_COMPLETE: 'EOR Complete', + INAUGURATION: 'Inauguration', + DISQUALIFIED: 'Disqualified' } as const; // Resignation Stages diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index 0dc4c05..b4899dd 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -14,6 +14,17 @@ export interface ApplicationAttributes { state: string | null; experienceYears: number | null; investmentCapacity: string | null; + age: number | null; + education: string | null; + companyName: string | null; + source: string | null; + existingDealer: string | null; + ownRoyalEnfield: string | null; + royalEnfieldModel: string | null; + description: string | null; + address: string | null; + pincode: string | null; + locationType: string | null; currentStage: string; overallStatus: string; progressPercentage: number; @@ -87,6 +98,50 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: true }, + age: { + type: DataTypes.INTEGER, + allowNull: true + }, + education: { + type: DataTypes.STRING, + allowNull: true + }, + companyName: { + type: DataTypes.STRING, + allowNull: true + }, + source: { + type: DataTypes.STRING, + allowNull: true + }, + existingDealer: { + type: DataTypes.STRING, // Storing 'yes'/'no' + allowNull: true + }, + ownRoyalEnfield: { + type: DataTypes.STRING, // Storing 'yes'/'no' + allowNull: true + }, + royalEnfieldModel: { + type: DataTypes.STRING, + allowNull: true + }, + description: { + type: DataTypes.TEXT, + allowNull: true + }, + address: { + type: DataTypes.TEXT, + allowNull: true + }, + pincode: { + type: DataTypes.STRING, + allowNull: true + }, + locationType: { + type: DataTypes.STRING, + allowNull: true + }, currentStage: { type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)), defaultValue: APPLICATION_STAGES.DD diff --git a/src/database/models/Area.ts b/src/database/models/Area.ts index 8f6acfa..4495d7f 100644 --- a/src/database/models/Area.ts +++ b/src/database/models/Area.ts @@ -6,11 +6,14 @@ export interface AreaAttributes { stateId: string; zoneId: string; districtId: string; + managerId: string | null; areaCode: string; areaName: string; city: string | null; pincode: string | null; isActive: boolean; + activeFrom?: string | null; + activeTo?: string | null; } export interface AreaInstance extends Model, AreaAttributes { } @@ -54,6 +57,14 @@ export default (sequelize: Sequelize) => { key: 'id' } }, + managerId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, areaCode: { type: DataTypes.STRING, unique: true, @@ -74,6 +85,14 @@ export default (sequelize: Sequelize) => { isActive: { type: DataTypes.BOOLEAN, defaultValue: true + }, + activeFrom: { + type: DataTypes.DATEONLY, + allowNull: true + }, + activeTo: { + type: DataTypes.DATEONLY, + allowNull: true } }, { tableName: 'areas', @@ -97,10 +116,25 @@ export default (sequelize: Sequelize) => { foreignKey: 'districtId', as: 'district' }); + Area.belongsTo(models.User, { + foreignKey: 'managerId', + as: 'manager' + }); Area.hasMany(models.Application, { foreignKey: 'areaId', as: 'applications' }); + // Dedicated Manager Table Associations + Area.hasMany(models.AreaManager, { + foreignKey: 'areaId', + as: 'areaManagers' + }); + Area.belongsToMany(models.User, { + through: models.AreaManager, + foreignKey: 'areaId', + otherKey: 'userId', + as: 'assignedManagers' + }); }; return Area; diff --git a/src/database/models/AreaManager.ts b/src/database/models/AreaManager.ts index 07a1139..9c60692 100644 --- a/src/database/models/AreaManager.ts +++ b/src/database/models/AreaManager.ts @@ -5,6 +5,7 @@ export interface AreaManagerAttributes { areaId: string; userId: string; managerType: string; + asmCode?: string; isActive: boolean; assignedAt: Date; } @@ -38,6 +39,10 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: false }, + asmCode: { + type: DataTypes.STRING, + allowNull: true + }, isActive: { type: DataTypes.BOOLEAN, defaultValue: true diff --git a/src/database/models/QuestionnaireQuestion.ts b/src/database/models/QuestionnaireQuestion.ts index 91c74db..1d0fc8c 100644 --- a/src/database/models/QuestionnaireQuestion.ts +++ b/src/database/models/QuestionnaireQuestion.ts @@ -8,6 +8,8 @@ export interface QuestionnaireQuestionAttributes { inputType: string; options: any; isMandatory: boolean; + weight: number; + order: number; } export interface QuestionnaireQuestionInstance extends Model, QuestionnaireQuestionAttributes { } @@ -46,6 +48,16 @@ export default (sequelize: Sequelize) => { isMandatory: { type: DataTypes.BOOLEAN, defaultValue: true + }, + weight: { + type: DataTypes.DECIMAL(5, 2), + defaultValue: 0, + allowNull: false + }, + order: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false } }, { tableName: 'questionnaire_questions', diff --git a/src/database/models/Region.ts b/src/database/models/Region.ts index dfb534a..db173a5 100644 --- a/src/database/models/Region.ts +++ b/src/database/models/Region.ts @@ -3,7 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface RegionAttributes { id: string; zoneId: string; - stateId: string | null; + // stateId: string | null; // Removed as Region covers multiple states + regionalManagerId: string | null; regionCode: string; regionName: string; description: string | null; @@ -27,11 +28,19 @@ export default (sequelize: Sequelize) => { key: 'id' } }, - stateId: { + // stateId: { + // type: DataTypes.UUID, + // allowNull: true, + // references: { + // model: 'states', + // key: 'id' + // } + // }, + regionalManagerId: { type: DataTypes.UUID, allowNull: true, references: { - model: 'states', + model: 'users', key: 'id' } }, @@ -62,9 +71,13 @@ export default (sequelize: Sequelize) => { foreignKey: 'zoneId', as: 'zone' }); - Region.belongsTo(models.State, { - foreignKey: 'stateId', - as: 'state' + // Region.belongsTo(models.State, { + // foreignKey: 'stateId', + // as: 'state' + // }); + Region.hasMany(models.State, { + foreignKey: 'regionId', + as: 'states' }); Region.hasMany(models.Area, { foreignKey: 'regionId', @@ -78,6 +91,10 @@ export default (sequelize: Sequelize) => { foreignKey: 'regionId', as: 'applications' }); + Region.belongsTo(models.User, { + foreignKey: 'regionalManagerId', + as: 'regionalManager' + }); }; return Region; diff --git a/src/database/models/State.ts b/src/database/models/State.ts index 1b1efed..fa04e83 100644 --- a/src/database/models/State.ts +++ b/src/database/models/State.ts @@ -4,6 +4,7 @@ export interface StateAttributes { id: string; stateName: string; zoneId: string; + regionId: string | null; isActive: boolean; } @@ -29,6 +30,14 @@ export default (sequelize: Sequelize) => { key: 'id' } }, + regionId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'regions', + key: 'id' + } + }, isActive: { type: DataTypes.BOOLEAN, defaultValue: true @@ -43,6 +52,10 @@ export default (sequelize: Sequelize) => { foreignKey: 'zoneId', as: 'zone' }); + State.belongsTo(models.Region, { + foreignKey: 'regionId', + as: 'region' + }); State.hasMany(models.District, { foreignKey: 'stateId', as: 'districts' diff --git a/src/database/models/User.ts b/src/database/models/User.ts index 4edfa2f..133623c 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -159,6 +159,7 @@ export default (sequelize: Sequelize) => { User.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' }); User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); + User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' }); }; return User; diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index f9c9d81..e5ebfc3 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; import db from '../../database/models/index.js'; const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db; import { AUDIT_ACTIONS } from '../../common/config/constants.js'; @@ -144,6 +145,68 @@ export const getAllUsers = async (req: Request, res: Response) => { console.error('Get users error:', error); res.status(500).json({ success: false, message: 'Error fetching users' }); } +} + +export const createUser = async (req: AuthRequest, res: Response) => { + try { + const { + fullName, email, roleCode, + employeeId, mobileNumber, department, designation, + zoneId, regionId, stateId, districtId, areaId + } = req.body; + + // Validate required fields + if (!fullName || !email || !roleCode) { + return res.status(400).json({ + success: false, + message: 'Full Name, Email, and Role are required' + }); + } + + // Check if user already exists + const existingUser = await User.findOne({ where: { email } }); + if (existingUser) { + return res.status(400).json({ + success: false, + message: 'User with this email already exists' + }); + } + + // Hash default password + const hashedPassword = await bcrypt.hash('Admin@123', 10); + + // Create user + const user = await User.create({ + fullName, + email, + password: hashedPassword, + roleCode, + status: 'active', + isActive: true, + employeeId, + mobileNumber, + department, + designation, + zoneId, + regionId, + stateId, + districtId, + areaId + }); + + await AuditLog.create({ + userId: req.user?.id, + action: AUDIT_ACTIONS.CREATED, + entityType: 'user', + entityId: user.id, + newData: req.body + }); + + res.status(201).json({ success: true, message: 'User created successfully', data: user }); + } catch (error) { + console.error('Create user error:', error); + res.status(500).json({ success: false, message: 'Error creating user' }); + } }; export const updateUserStatus = async (req: AuthRequest, res: Response) => { @@ -177,14 +240,15 @@ export const updateUser = async (req: AuthRequest, res: Response) => { const { fullName, email, roleCode, status, isActive, employeeId, mobileNumber, department, designation, - zoneId, regionId, stateId, districtId, areaId + zoneId, regionId, stateId, districtId, areaId, + password // Optional password update } = req.body; const user = await User.findByPk(id); if (!user) return res.status(404).json({ success: false, message: 'User not found' }); const oldData = user.toJSON(); - await user.update({ + const updates: any = { fullName: fullName || user.fullName, email: email || user.email, roleCode: roleCode || user.roleCode, @@ -199,7 +263,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => { stateId: stateId !== undefined ? stateId : user.stateId, districtId: districtId !== undefined ? districtId : user.districtId, areaId: areaId !== undefined ? areaId : user.areaId - }); + }; + + // If password is provided, hash it and update + if (password && password.trim() !== '') { + updates.password = await bcrypt.hash(password, 10); + } + + await user.update(updates); await AuditLog.create({ userId: req.user?.id, diff --git a/src/modules/admin/admin.routes.ts b/src/modules/admin/admin.routes.ts index 43dd63c..bcfef5a 100644 --- a/src/modules/admin/admin.routes.ts +++ b/src/modules/admin/admin.routes.ts @@ -18,6 +18,7 @@ router.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController. router.get('/permissions', adminController.getPermissions); // Users (Admin View) +router.post('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.createUser); router.get('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getAllUsers); 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); diff --git a/src/modules/master/master.controller.ts b/src/modules/master/master.controller.ts index 754f9d8..cb9c80a 100644 --- a/src/modules/master/master.controller.ts +++ b/src/modules/master/master.controller.ts @@ -1,11 +1,28 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { Region, Zone, State, District, Area, User } = db; +const { Region, Zone, State, District, Area, User, AreaManager } = db; // --- Regions --- export const getRegions = async (req: Request, res: Response) => { try { const regions = await Region.findAll({ + include: [ + { + model: State, + as: 'states', + attributes: ['id', 'stateName'] + }, + { + model: Zone, + as: 'zone', + attributes: ['id', 'zoneName'] + }, + { + model: User, + as: 'regionalManager', + attributes: ['id', 'fullName', 'email', 'mobileNumber'] + } + ], order: [['regionName', 'ASC']] }); @@ -18,13 +35,27 @@ export const getRegions = async (req: Request, res: Response) => { export const createRegion = async (req: Request, res: Response) => { try { - const { regionName } = req.body; + const { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body; - if (!regionName) { - return res.status(400).json({ success: false, message: 'Region name is required' }); + if (!zoneId || !regionName || !regionCode) { + return res.status(400).json({ success: false, message: 'Zone ID, region name and code are required' }); } - const region = await Region.create({ regionName }); + const region = await Region.create({ + zoneId, + regionCode, + regionName, + description, + regionalManagerId: regionalManagerId || null + }); + + // Assign states if provided + if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { + await State.update( + { regionId: region.id, zoneId }, // Also ensure State belongs to the Zone (hierarchy) + { where: { id: stateIds } } + ); + } res.status(201).json({ success: true, message: 'Region created successfully', data: region }); } catch (error) { @@ -36,17 +67,59 @@ export const createRegion = async (req: Request, res: Response) => { export const updateRegion = async (req: Request, res: Response) => { try { const { id } = req.params; - const { regionName } = req.body; + const { zoneId, regionCode, regionName, description, isActive, stateIds, regionalManagerId } = req.body; const region = await Region.findByPk(id); if (!region) { return res.status(404).json({ success: false, message: 'Region not found' }); } - await region.update({ - regionName: regionName || (region as any).regionName, - updatedAt: new Date() - }); + const updates: any = {}; + if (zoneId) updates.zoneId = zoneId; + if (regionCode) updates.regionCode = regionCode; + if (regionName) updates.regionName = regionName; + if (description !== undefined) updates.description = description; + if (isActive !== undefined) updates.isActive = isActive; + if (regionalManagerId !== undefined) updates.regionalManagerId = regionalManagerId; + + await region.update(updates); + + // Handle State reassignment + if (stateIds && Array.isArray(stateIds)) { + // 1. Unassign states currently assigned to this region but NOT in the new list? + // Or just simpler: Assign the new ones. Old ones stay? + // Standard behavior for "List of items in a container": Sync list. + // We should set regionId=null for states previously in this region but not in stateIds. + // But let's check safety. If I uncheck a state, I want it removed from the region. + + // First, find states currently in this region + // Actually, simplest 'Reset and Set' approach: + // 1. Set regionId=null for all states where regionId = this.id + // 2. Set regionId=this.id for states in stateIds. + + // Note: We should probably also enforce zoneId match? + // If a user moves a state to this Region, the State must conceptually belong to the Region's Zone. + // So we update both regionId and zoneId for the target states. + + // Step 1: Remove States from this Region (if they are NOT in the new list) + // We can do this by: + // await State.update({ regionId: null }, { where: { regionId: id } }); + // But wait, if I am only ADDING, I don't want to nuke everything. + // But "update" implies "this is the new state of the world". + // Assuming frontend sends the FULL list of selected states. + + await State.update({ regionId: null }, { where: { regionId: id } }); + + if (stateIds.length > 0) { + await State.update( + { + regionId: id, + zoneId: zoneId || region.zoneId // Ensure state moves to the region's zone + }, + { where: { id: stateIds } } + ); + } + } res.json({ success: true, message: 'Region updated successfully' }); } catch (error) { @@ -117,17 +190,33 @@ export const createZone = async (req: Request, res: Response) => { export const updateZone = async (req: Request, res: Response) => { try { const { id } = req.params; - const { zoneName } = req.body; + const { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body; const zone = await Zone.findByPk(id); if (!zone) { return res.status(404).json({ success: false, message: 'Zone not found' }); } - await zone.update({ - zoneName: zoneName || (zone as any).zoneName, - updatedAt: new Date() - }); + const updates: any = {}; + if (zoneName) updates.zoneName = zoneName; + if (description !== undefined) updates.description = description; + if (isActive !== undefined) updates.isActive = isActive; + if (zonalBusinessHeadId !== undefined) updates.zonalBusinessHeadId = zonalBusinessHeadId; + + await zone.update(updates); + + // Handle State assignment + if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { + // Update all provided states to belong to this zone + // We can't easily "remove" states because zoneId is non-nullable. + // States must be moved TO another zone to be removed from this one. + // So we primarily handle "bringing states into this zone". + // However, we should check if they exist first. + await State.update( + { zoneId: zone.id }, + { where: { id: stateIds } } + ); + } res.json({ success: true, message: 'Zone updated successfully' }); } catch (error) { @@ -239,7 +328,26 @@ export const getAreas = async (req: Request, res: Response) => { const areas = await Area.findAll({ where, - include: [{ model: District, as: 'district', attributes: ['districtName'] }], + include: [ + { model: District, as: 'district', attributes: ['districtName'] }, + { model: State, as: 'state', attributes: ['stateName'] }, + { model: Region, as: 'region', attributes: ['regionName'] }, + { model: Zone, as: 'zone', attributes: ['zoneName'] }, + // Include explicit manager column (legacy/fallback) + { model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] }, + // Include active managers from dedicated table + { + model: AreaManager, + as: 'areaManagers', + where: { isActive: true }, + required: false, // Left join, so we get areas even without managers + include: [{ + model: User, + as: 'user', + attributes: ['id', 'fullName', 'email', 'mobileNumber'] + }] + } + ], order: [['areaName', 'ASC']] }); res.json({ success: true, areas }); @@ -251,7 +359,7 @@ export const getAreas = async (req: Request, res: Response) => { export const createArea = async (req: Request, res: Response) => { try { - const { districtId, areaCode, areaName, city, pincode } = req.body; + const { districtId, areaCode, areaName, city, pincode, managerId } = req.body; if (!districtId || !areaName || !pincode) return res.status(400).json({ success: false, message: 'District ID, area name, and pincode required' }); // Need to fetch regionId from district -> state -> zone -> region? @@ -259,22 +367,60 @@ export const createArea = async (req: Request, res: Response) => { // The Area model has regionId, districtId. // It's safer to fetch relationships. const district = await District.findByPk(districtId, { - include: [{ model: State, include: [{ model: Zone, include: [{ model: Region }] }] }] + include: [{ + model: State, + as: 'state', + include: [ + { model: Zone, as: 'zone' }, + { model: Region, as: 'region' } + ] + }] }); let regionId = null; - if (district && district.state && district.state.zone && district.state.zone.region) { - regionId = district.state.zone.region.id; + let zoneId = null; + let stateId = null; + + if (district) { + stateId = district.stateId; + // Access associations using the logical structure (District -> State -> Zone/Region) + if (district.state) { + if (district.state.zone) { + zoneId = district.state.zone.id; + } + if (district.state.region) { + regionId = district.state.region.id; + } + } } const area = await Area.create({ districtId, + stateId, + zoneId, regionId, areaCode, areaName, city, - pincode + pincode, + managerId: managerId || null, // Legacy support + isActive: req.body.isActive ?? true, + activeFrom: req.body.activeFrom || null, + activeTo: req.body.activeTo || null }); + + // Create AreaManager record if manager assigned + if (managerId) { + await AreaManager.create({ + areaId: area.id, + userId: managerId, + managerType: 'ASM', + isActive: true, + assignedAt: new Date(), + asmCode: req.body.asmCode || null + }); + } + res.status(201).json({ success: true, message: 'Area created', data: area }); } catch (error) { console.error('Create area error:', error); @@ -282,14 +428,111 @@ export const createArea = async (req: Request, res: Response) => { } }; +// --- Area Managers --- +export const getAreaManagers = async (req: Request, res: Response) => { + try { + // Fetch Users who have active AreaManager assignments + // We use the User model as the primary so we get the User details naturally + const managers = await User.findAll({ + attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId', 'roleCode', 'zoneId', 'regionId'], + include: [ + { + model: AreaManager, + as: 'areaManagers', + where: { isActive: true }, + required: true, // Only return users who ARE active managers + attributes: ['asmCode'], + include: [ + { + model: Area, + as: 'area', + attributes: ['id', 'areaName', 'areaCode'], + include: [ + { model: District, as: 'district', attributes: ['districtName'] }, + { model: State, as: 'state', attributes: ['stateName'] }, + { model: Region, as: 'region', attributes: ['id', 'regionName'] }, + { model: Zone, as: 'zone', attributes: ['id', 'zoneName'] } + ] + } + ] + }, + { model: Zone, as: 'zone', attributes: ['id', 'zoneName'] }, + { model: Region, as: 'region', attributes: ['id', 'regionName'] } + ], + order: [['fullName', 'ASC']] + }); + + // Transform if necessary to flatten the structure for the frontend + // But the user asked for "straightforward", so a clean nested JSON is usually best + // We can double check if they want a flat list of (User, Area) pairs or User -> [Areas] + // "Arean mangers" implies the People. So User -> [Areas] is the best entity representation. + + res.json({ success: true, data: managers }); + } catch (error) { + console.error('Get area managers error:', error); + res.status(500).json({ success: false, message: 'Error fetching area managers' }); + } +}; + export const updateArea = async (req: Request, res: Response) => { try { const { id } = req.params; - const { areaName, city, pincode, isActive } = req.body; + const { areaName, city, pincode, isActive, managerId } = req.body; const area = await Area.findByPk(id); if (!area) return res.status(404).json({ success: false, message: 'Area not found' }); - await area.update({ areaName, city, pincode, isActive }); + const updates: any = {}; + if (areaName) updates.areaName = areaName; + if (city) updates.city = city; + if (pincode) updates.pincode = pincode; + if (isActive !== undefined) updates.isActive = isActive; + if (managerId !== undefined) updates.managerId = managerId; // Legacy support + + await area.update(updates); + + // Handle AreaManager Table Update + if (managerId !== undefined) { + const asmCode = req.body.asmCode; + + // 1. Find currently active manager for this area + const currentActiveManager = await AreaManager.findOne({ + where: { + areaId: id, + isActive: true + } + }); + + // If there is an active manager + if (currentActiveManager) { + // If the new managerId is different (or null, meaning unassign), deactivate the old one + if (currentActiveManager.userId !== managerId) { + await currentActiveManager.update({ isActive: false }); + } else { + // If SAME user, update asmCode if provided + if (asmCode !== undefined) { + await currentActiveManager.update({ asmCode }); + } + } + } + + // 2. If a new manager is being assigned (and it's not null) + if (managerId) { + // Check if this specific user is already active (to avoid duplicates if logic above missed it) + const isAlreadyActive = currentActiveManager && currentActiveManager.userId === managerId; + + if (!isAlreadyActive) { + await AreaManager.create({ + areaId: id, + userId: managerId, + managerType: 'ASM', // Default type + isActive: true, + assignedAt: new Date(), + asmCode: asmCode || null + }); + } + } + } + res.json({ success: true, message: 'Area updated' }); } catch (error) { console.error('Update area error:', error); diff --git a/src/modules/master/master.routes.ts b/src/modules/master/master.routes.ts index 0713832..94e16f7 100644 --- a/src/modules/master/master.routes.ts +++ b/src/modules/master/master.routes.ts @@ -6,6 +6,11 @@ import { authenticate } from '../../common/middleware/auth.js'; import { checkRole } from '../../common/middleware/roleCheck.js'; import { ROLES } from '../../common/config/constants.js'; +// States +router.get('/states', masterController.getStates); +// Districts +router.get('/districts', masterController.getDistricts); + // All routes require authentication router.use(authenticate as any); @@ -19,13 +24,11 @@ router.get('/zones', masterController.getZones); router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone); router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateZone); -// States -router.get('/states', masterController.getStates); +// States (Update only) router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState); router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateState); -// Districts -router.get('/districts', masterController.getDistricts); +// Districts (Update only) router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict); router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateDistrict); @@ -34,6 +37,9 @@ router.get('/areas', masterController.getAreas); router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea); router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateArea); +// Area Managers +router.get('/area-managers', masterController.getAreaManagers); + // Outlets router.get('/outlets', outletController.getOutlets); router.get('/outlets/:id', outletController.getOutletById); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 4f69bb0..ba95394 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog } = db; -import { AUDIT_ACTIONS } from '../../common/config/constants.js'; +const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone } = db; +import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js'; import { v4 as uuidv4 } from 'uuid'; import { Op } from 'sequelize'; import { AuthRequest } from '../../types/express.types.js'; @@ -11,7 +11,8 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { const { opportunityId, applicantName, email, phone, businessType, locationType, - preferredLocation, city, state, experienceYears, investmentCapacity + preferredLocation, city, state, experienceYears, investmentCapacity, + age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode } = req.body; // Check for duplicate email @@ -23,13 +24,34 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`; // Fetch hierarchy from Opportunity if available + // Fetch hierarchy from Opportunity if available, OR resolve from Location let zoneId, regionId, areaId; + if (opportunityId) { const opportunity = await Opportunity.findByPk(opportunityId); if (opportunity) { zoneId = opportunity.zoneId; regionId = opportunity.regionId; - // areaId might need manual assignment or derived + } + } else if (req.body.district) { + // Resolve hierarchy from submitted District + const districtName = req.body.district; + const districtRecord = await District.findOne({ + where: { + districtName: { [Op.iLike]: districtName } // Case-insensitive match + }, + include: [ + { model: Region, as: 'region', attributes: ['id', 'zoneId'] }, + { model: Zone, as: 'zone', attributes: ['id'] } + ] + }); + + if (districtRecord) { + regionId = districtRecord.regionId; + zoneId = districtRecord.zoneId || (districtRecord.region ? districtRecord.region.zoneId : null); + console.log(`Auto-assigned Application to Region: ${regionId}, Zone: ${zoneId} based on District: ${districtName}`); + } else { + console.log(`Could not find District: ${districtName} for auto-assignment.`); } } @@ -45,8 +67,9 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { state, experienceYears, investmentCapacity, - currentStage: 'Application', - overallStatus: 'New', + age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, + currentStage: APPLICATION_STAGES.DD, + overallStatus: APPLICATION_STATUS.PENDING, progressPercentage: 10, zoneId, regionId @@ -56,7 +79,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { await ApplicationStatusHistory.create({ applicationId: application.id, previousStatus: null, - newStatus: 'New', + newStatus: APPLICATION_STATUS.PENDING, changedBy: req.user?.id, reason: 'Initial Submission' }); @@ -65,6 +88,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { await ApplicationProgress.create({ applicationId: application.id, stageName: 'Application', + stageOrder: 1, status: 'Completed', completionPercentage: 100 }); @@ -91,7 +115,7 @@ export const getApplications = async (req: Request, res: Response) => { try { // Add filtering logic here similar to Opportunity const applications = await Application.findAll({ - include: [{ model: Opportunity, as: 'opportunity', attributes: ['leadName', 'id'] }], + include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }], order: [['createdAt', 'DESC']] }); @@ -115,7 +139,7 @@ export const getApplicationById = async (req: Request, res: Response) => { }, include: [ { model: ApplicationStatusHistory, as: 'statusHistory' }, - { model: ApplicationProgress, as: 'progress' } + { model: ApplicationProgress, as: 'progressTracking' } ] }); diff --git a/src/modules/onboarding/onboarding.routes.ts b/src/modules/onboarding/onboarding.routes.ts index 6b93fe9..fc11324 100644 --- a/src/modules/onboarding/onboarding.routes.ts +++ b/src/modules/onboarding/onboarding.routes.ts @@ -4,9 +4,12 @@ import * as onboardingController from './onboarding.controller.js'; import { authenticate } from '../../common/middleware/auth.js'; // All routes require authentication (or public for submission? Keeping auth for now) +// Public route for application submission +router.post('/apply', onboardingController.submitApplication); + +// All subsequent routes require authentication router.use(authenticate as any); -router.post('/apply', onboardingController.submitApplication); router.get('/applications', onboardingController.getApplications); router.get('/applications/:id', onboardingController.getApplicationById); router.put('/applications/:id/status', onboardingController.updateApplicationStatus); diff --git a/src/modules/onboarding/questionnaire.controller.ts b/src/modules/onboarding/questionnaire.controller.ts new file mode 100644 index 0000000..55aa811 --- /dev/null +++ b/src/modules/onboarding/questionnaire.controller.ts @@ -0,0 +1,102 @@ +import { Request, Response } from 'express'; +import db from '../../database/models/index.js'; +const { Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, Application } = db; +import { v4 as uuidv4 } from 'uuid'; +import { AuthRequest } from '../../types/express.types.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']] + }], + 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) { + const questionRecords = questions.map((q: any, index: number) => ({ + questionnaireId: newQuestionnaire.id, + sectionName: q.sectionName || 'General', + questionText: q.questionText, + inputType: q.inputType || 'text', + options: q.options || null, + weight: q.weight || 0, + order: q.order || index + 1, + isMandatory: q.isMandatory !== false + })); + + await QuestionnaireQuestion.bulkCreate(questionRecords); + } + + const fullQuestionnaire = await Questionnaire.findByPk(newQuestionnaire.id, { + include: [{ model: QuestionnaireQuestion, as: 'questions' }] + }); + + 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 }] + + // Verify application + const application = await Application.findByPk(applicationId); + 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, + 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); + + 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' }); + } +}; diff --git a/src/modules/onboarding/questionnaire.routes.ts b/src/modules/onboarding/questionnaire.routes.ts new file mode 100644 index 0000000..4b46de6 --- /dev/null +++ b/src/modules/onboarding/questionnaire.routes.ts @@ -0,0 +1,17 @@ +import express from 'express'; +const router = express.Router(); +import * as questionnaireController from './questionnaire.controller.js'; +import { authenticate } from '../../common/middleware/auth.js'; +import { checkRole } from '../../common/middleware/roleCheck.js'; +import { ROLES } from '../../common/config/constants.js'; + +router.use(authenticate as any); + +// Public/Dealer routes (Application context) +router.get('/latest', questionnaireController.getLatestQuestionnaire); +router.post('/response', questionnaireController.submitResponse); + +// Admin routes +router.post('/version', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_HEAD]), questionnaireController.createQuestionnaireVersion); + +export default router; diff --git a/src/server.ts b/src/server.ts index 246bb24..8f05cf5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -31,6 +31,7 @@ import eorRoutes from './modules/eor/eor.routes.js'; import dealerRoutes from './modules/dealer/dealer.routes.js'; import slaRoutes from './modules/sla/sla.routes.js'; import communicationRoutes from './modules/communication/communication.routes.js'; +import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js'; // Import common middleware & utils import errorHandler from './common/middleware/errorHandler.js'; @@ -52,7 +53,7 @@ app.use(cors({ // Rate limiting const limiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes - max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), + max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000'), message: 'Too many requests from this IP, please try again later.' }); app.use('/api/', limiter); @@ -104,6 +105,7 @@ app.use('/api/eor', eorRoutes); app.use('/api/dealer', dealerRoutes); app.use('/api/sla', slaRoutes); app.use('/api/communication', communicationRoutes); +app.use('/api/questionnaire', questionnaireRoutes); // Backward Compatibility Aliases app.use('/api/applications', onboardingRoutes); @@ -144,7 +146,7 @@ const startServer = async () => { // Sync database (in development only) if (process.env.NODE_ENV === 'development') { - await db.sequelize.sync({ alter: false }); + await db.sequelize.sync({ alter: true }); logger.info('Database models synchronized'); }