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:
parent
f54501793c
commit
5959a6a225
@ -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
|
||||
%% ============================================
|
||||
|
||||
95
docs/project_status_report.md
Normal file
95
docs/project_status_report.md
Normal 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.*
|
||||
63
scripts/debug-area-manager.ts
Normal file
63
scripts/debug-area-manager.ts
Normal 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
26
scripts/fix-asm-column.ts
Normal 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
21
scripts/force-sync.ts
Normal 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
21
scripts/test-areas.ts
Normal 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
34
scripts/test-regions.ts
Normal 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();
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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' }
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
102
src/modules/onboarding/questionnaire.controller.ts
Normal file
102
src/modules/onboarding/questionnaire.controller.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
17
src/modules/onboarding/questionnaire.routes.ts
Normal file
17
src/modules/onboarding/questionnaire.routes.ts
Normal 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;
|
||||
@ -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');
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user