from backend areaa manger table alterd made it redy to recive the dealer application form and alo now api serving aplications list and application detail

This commit is contained in:
laxmanhalaki 2026-01-28 21:38:32 +05:30
parent f54501793c
commit 5959a6a225
24 changed files with 961 additions and 49 deletions

View File

@ -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
%% ============================================

View File

@ -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.*

View File

@ -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();

26
scripts/fix-asm-column.ts Normal file
View File

@ -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();

21
scripts/force-sync.ts Normal file
View File

@ -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();

21
scripts/test-areas.ts Normal file
View File

@ -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();

34
scripts/test-regions.ts Normal file
View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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>, 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;

View File

@ -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

View File

@ -8,6 +8,8 @@ export interface QuestionnaireQuestionAttributes {
inputType: string;
options: any;
isMandatory: boolean;
weight: number;
order: number;
}
export interface QuestionnaireQuestionInstance extends Model<QuestionnaireQuestionAttributes>, 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',

View File

@ -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;

View File

@ -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'

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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' }
]
});

View File

@ -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);

View File

@ -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' });
}
};

View File

@ -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;

View File

@ -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');
}