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 area_id FK
|
||||||
uuid assigned_dd_zm FK
|
uuid assigned_dd_zm FK
|
||||||
uuid assigned_rbm FK
|
uuid assigned_rbm FK
|
||||||
|
json documents
|
||||||
|
json timeline
|
||||||
|
integer progress_percentage
|
||||||
timestamp submitted_at
|
timestamp submitted_at
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
timestamp updated_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
|
%% 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',
|
PENDING: 'Pending',
|
||||||
IN_REVIEW: 'In Review',
|
IN_REVIEW: 'In Review',
|
||||||
APPROVED: 'Approved',
|
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;
|
} as const;
|
||||||
|
|
||||||
// Resignation Stages
|
// Resignation Stages
|
||||||
|
|||||||
@ -14,6 +14,17 @@ export interface ApplicationAttributes {
|
|||||||
state: string | null;
|
state: string | null;
|
||||||
experienceYears: number | null;
|
experienceYears: number | null;
|
||||||
investmentCapacity: string | 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;
|
currentStage: string;
|
||||||
overallStatus: string;
|
overallStatus: string;
|
||||||
progressPercentage: number;
|
progressPercentage: number;
|
||||||
@ -87,6 +98,50 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
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: {
|
currentStage: {
|
||||||
type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)),
|
type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)),
|
||||||
defaultValue: APPLICATION_STAGES.DD
|
defaultValue: APPLICATION_STAGES.DD
|
||||||
|
|||||||
@ -6,11 +6,14 @@ export interface AreaAttributes {
|
|||||||
stateId: string;
|
stateId: string;
|
||||||
zoneId: string;
|
zoneId: string;
|
||||||
districtId: string;
|
districtId: string;
|
||||||
|
managerId: string | null;
|
||||||
areaCode: string;
|
areaCode: string;
|
||||||
areaName: string;
|
areaName: string;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
pincode: string | null;
|
pincode: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
activeFrom?: string | null;
|
||||||
|
activeTo?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AreaInstance extends Model<AreaAttributes>, AreaAttributes { }
|
export interface AreaInstance extends Model<AreaAttributes>, AreaAttributes { }
|
||||||
@ -54,6 +57,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
managerId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
areaCode: {
|
areaCode: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
unique: true,
|
unique: true,
|
||||||
@ -74,6 +85,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
|
},
|
||||||
|
activeFrom: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
activeTo: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'areas',
|
tableName: 'areas',
|
||||||
@ -97,10 +116,25 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'districtId',
|
foreignKey: 'districtId',
|
||||||
as: 'district'
|
as: 'district'
|
||||||
});
|
});
|
||||||
|
Area.belongsTo(models.User, {
|
||||||
|
foreignKey: 'managerId',
|
||||||
|
as: 'manager'
|
||||||
|
});
|
||||||
Area.hasMany(models.Application, {
|
Area.hasMany(models.Application, {
|
||||||
foreignKey: 'areaId',
|
foreignKey: 'areaId',
|
||||||
as: 'applications'
|
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;
|
return Area;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface AreaManagerAttributes {
|
|||||||
areaId: string;
|
areaId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
managerType: string;
|
managerType: string;
|
||||||
|
asmCode?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
assignedAt: Date;
|
assignedAt: Date;
|
||||||
}
|
}
|
||||||
@ -38,6 +39,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
|
asmCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export interface QuestionnaireQuestionAttributes {
|
|||||||
inputType: string;
|
inputType: string;
|
||||||
options: any;
|
options: any;
|
||||||
isMandatory: boolean;
|
isMandatory: boolean;
|
||||||
|
weight: number;
|
||||||
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionnaireQuestionInstance extends Model<QuestionnaireQuestionAttributes>, QuestionnaireQuestionAttributes { }
|
export interface QuestionnaireQuestionInstance extends Model<QuestionnaireQuestionAttributes>, QuestionnaireQuestionAttributes { }
|
||||||
@ -46,6 +48,16 @@ export default (sequelize: Sequelize) => {
|
|||||||
isMandatory: {
|
isMandatory: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
type: DataTypes.DECIMAL(5, 2),
|
||||||
|
defaultValue: 0,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0,
|
||||||
|
allowNull: false
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'questionnaire_questions',
|
tableName: 'questionnaire_questions',
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
|||||||
export interface RegionAttributes {
|
export interface RegionAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
zoneId: string;
|
zoneId: string;
|
||||||
stateId: string | null;
|
// stateId: string | null; // Removed as Region covers multiple states
|
||||||
|
regionalManagerId: string | null;
|
||||||
regionCode: string;
|
regionCode: string;
|
||||||
regionName: string;
|
regionName: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@ -27,11 +28,19 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stateId: {
|
// stateId: {
|
||||||
|
// type: DataTypes.UUID,
|
||||||
|
// allowNull: true,
|
||||||
|
// references: {
|
||||||
|
// model: 'states',
|
||||||
|
// key: 'id'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
regionalManagerId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'states',
|
model: 'users',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -62,9 +71,13 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'zoneId',
|
foreignKey: 'zoneId',
|
||||||
as: 'zone'
|
as: 'zone'
|
||||||
});
|
});
|
||||||
Region.belongsTo(models.State, {
|
// Region.belongsTo(models.State, {
|
||||||
foreignKey: 'stateId',
|
// foreignKey: 'stateId',
|
||||||
as: 'state'
|
// as: 'state'
|
||||||
|
// });
|
||||||
|
Region.hasMany(models.State, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'states'
|
||||||
});
|
});
|
||||||
Region.hasMany(models.Area, {
|
Region.hasMany(models.Area, {
|
||||||
foreignKey: 'regionId',
|
foreignKey: 'regionId',
|
||||||
@ -78,6 +91,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'regionId',
|
foreignKey: 'regionId',
|
||||||
as: 'applications'
|
as: 'applications'
|
||||||
});
|
});
|
||||||
|
Region.belongsTo(models.User, {
|
||||||
|
foreignKey: 'regionalManagerId',
|
||||||
|
as: 'regionalManager'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return Region;
|
return Region;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export interface StateAttributes {
|
|||||||
id: string;
|
id: string;
|
||||||
stateName: string;
|
stateName: string;
|
||||||
zoneId: string;
|
zoneId: string;
|
||||||
|
regionId: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +30,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'regions',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
@ -43,6 +52,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'zoneId',
|
foreignKey: 'zoneId',
|
||||||
as: 'zone'
|
as: 'zone'
|
||||||
});
|
});
|
||||||
|
State.belongsTo(models.Region, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region'
|
||||||
|
});
|
||||||
State.hasMany(models.District, {
|
State.hasMany(models.District, {
|
||||||
foreignKey: 'stateId',
|
foreignKey: 'stateId',
|
||||||
as: 'districts'
|
as: 'districts'
|
||||||
|
|||||||
@ -159,6 +159,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
User.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
|
User.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
|
||||||
|
|
||||||
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
||||||
|
User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return User;
|
return User;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
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);
|
console.error('Get users error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error fetching users' });
|
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) => {
|
export const updateUserStatus = async (req: AuthRequest, res: Response) => {
|
||||||
@ -177,14 +240,15 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
fullName, email, roleCode, status, isActive, employeeId,
|
fullName, email, roleCode, status, isActive, employeeId,
|
||||||
mobileNumber, department, designation,
|
mobileNumber, department, designation,
|
||||||
zoneId, regionId, stateId, districtId, areaId
|
zoneId, regionId, stateId, districtId, areaId,
|
||||||
|
password // Optional password update
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
if (!user) return res.status(404).json({ success: false, message: 'User not found' });
|
if (!user) return res.status(404).json({ success: false, message: 'User not found' });
|
||||||
|
|
||||||
const oldData = user.toJSON();
|
const oldData = user.toJSON();
|
||||||
await user.update({
|
const updates: any = {
|
||||||
fullName: fullName || user.fullName,
|
fullName: fullName || user.fullName,
|
||||||
email: email || user.email,
|
email: email || user.email,
|
||||||
roleCode: roleCode || user.roleCode,
|
roleCode: roleCode || user.roleCode,
|
||||||
@ -199,7 +263,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
stateId: stateId !== undefined ? stateId : user.stateId,
|
stateId: stateId !== undefined ? stateId : user.stateId,
|
||||||
districtId: districtId !== undefined ? districtId : user.districtId,
|
districtId: districtId !== undefined ? districtId : user.districtId,
|
||||||
areaId: areaId !== undefined ? areaId : user.areaId
|
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({
|
await AuditLog.create({
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ router.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.
|
|||||||
router.get('/permissions', adminController.getPermissions);
|
router.get('/permissions', adminController.getPermissions);
|
||||||
|
|
||||||
// Users (Admin View)
|
// 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.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.patch('/users/:id/status', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUserStatus);
|
||||||
router.put('/users/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUser);
|
router.put('/users/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUser);
|
||||||
|
|||||||
@ -1,11 +1,28 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Region, Zone, State, District, Area, User } = db;
|
const { Region, Zone, State, District, Area, User, AreaManager } = db;
|
||||||
|
|
||||||
// --- Regions ---
|
// --- Regions ---
|
||||||
export const getRegions = async (req: Request, res: Response) => {
|
export const getRegions = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const regions = await 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']]
|
order: [['regionName', 'ASC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -18,13 +35,27 @@ export const getRegions = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const createRegion = async (req: Request, res: Response) => {
|
export const createRegion = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { regionName } = req.body;
|
const { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body;
|
||||||
|
|
||||||
if (!regionName) {
|
if (!zoneId || !regionName || !regionCode) {
|
||||||
return res.status(400).json({ success: false, message: 'Region name is required' });
|
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 });
|
res.status(201).json({ success: true, message: 'Region created successfully', data: region });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -36,17 +67,59 @@ export const createRegion = async (req: Request, res: Response) => {
|
|||||||
export const updateRegion = async (req: Request, res: Response) => {
|
export const updateRegion = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { regionName } = req.body;
|
const { zoneId, regionCode, regionName, description, isActive, stateIds, regionalManagerId } = req.body;
|
||||||
|
|
||||||
const region = await Region.findByPk(id);
|
const region = await Region.findByPk(id);
|
||||||
if (!region) {
|
if (!region) {
|
||||||
return res.status(404).json({ success: false, message: 'Region not found' });
|
return res.status(404).json({ success: false, message: 'Region not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await region.update({
|
const updates: any = {};
|
||||||
regionName: regionName || (region as any).regionName,
|
if (zoneId) updates.zoneId = zoneId;
|
||||||
updatedAt: new Date()
|
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' });
|
res.json({ success: true, message: 'Region updated successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,17 +190,33 @@ export const createZone = async (req: Request, res: Response) => {
|
|||||||
export const updateZone = async (req: Request, res: Response) => {
|
export const updateZone = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { zoneName } = req.body;
|
const { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body;
|
||||||
|
|
||||||
const zone = await Zone.findByPk(id);
|
const zone = await Zone.findByPk(id);
|
||||||
if (!zone) {
|
if (!zone) {
|
||||||
return res.status(404).json({ success: false, message: 'Zone not found' });
|
return res.status(404).json({ success: false, message: 'Zone not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await zone.update({
|
const updates: any = {};
|
||||||
zoneName: zoneName || (zone as any).zoneName,
|
if (zoneName) updates.zoneName = zoneName;
|
||||||
updatedAt: new Date()
|
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' });
|
res.json({ success: true, message: 'Zone updated successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -239,7 +328,26 @@ export const getAreas = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const areas = await Area.findAll({
|
const areas = await Area.findAll({
|
||||||
where,
|
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']]
|
order: [['areaName', 'ASC']]
|
||||||
});
|
});
|
||||||
res.json({ success: true, areas });
|
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) => {
|
export const createArea = async (req: Request, res: Response) => {
|
||||||
try {
|
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' });
|
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?
|
// 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.
|
// The Area model has regionId, districtId.
|
||||||
// It's safer to fetch relationships.
|
// It's safer to fetch relationships.
|
||||||
const district = await District.findByPk(districtId, {
|
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;
|
let regionId = null;
|
||||||
if (district && district.state && district.state.zone && district.state.zone.region) {
|
let zoneId = null;
|
||||||
regionId = district.state.zone.region.id;
|
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({
|
const area = await Area.create({
|
||||||
districtId,
|
districtId,
|
||||||
|
stateId,
|
||||||
|
zoneId,
|
||||||
regionId,
|
regionId,
|
||||||
areaCode,
|
areaCode,
|
||||||
areaName,
|
areaName,
|
||||||
city,
|
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 });
|
res.status(201).json({ success: true, message: 'Area created', data: area });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create area error:', 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) => {
|
export const updateArea = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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);
|
const area = await Area.findByPk(id);
|
||||||
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
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' });
|
res.json({ success: true, message: 'Area updated' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update area error:', 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 { checkRole } from '../../common/middleware/roleCheck.js';
|
||||||
import { ROLES } from '../../common/config/constants.js';
|
import { ROLES } from '../../common/config/constants.js';
|
||||||
|
|
||||||
|
// States
|
||||||
|
router.get('/states', masterController.getStates);
|
||||||
|
// Districts
|
||||||
|
router.get('/districts', masterController.getDistricts);
|
||||||
|
|
||||||
// All routes require authentication
|
// All routes require authentication
|
||||||
router.use(authenticate as any);
|
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.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);
|
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateZone);
|
||||||
|
|
||||||
// States
|
// States (Update only)
|
||||||
router.get('/states', masterController.getStates);
|
|
||||||
router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState);
|
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);
|
router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateState);
|
||||||
|
|
||||||
// Districts
|
// Districts (Update only)
|
||||||
router.get('/districts', masterController.getDistricts);
|
|
||||||
router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict);
|
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);
|
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.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);
|
router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateArea);
|
||||||
|
|
||||||
|
// Area Managers
|
||||||
|
router.get('/area-managers', masterController.getAreaManagers);
|
||||||
|
|
||||||
// Outlets
|
// Outlets
|
||||||
router.get('/outlets', outletController.getOutlets);
|
router.get('/outlets', outletController.getOutlets);
|
||||||
router.get('/outlets/:id', outletController.getOutletById);
|
router.get('/outlets/:id', outletController.getOutletById);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog } = db;
|
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone } = db;
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
@ -11,7 +11,8 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
opportunityId,
|
opportunityId,
|
||||||
applicantName, email, phone, businessType, locationType,
|
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;
|
} = req.body;
|
||||||
|
|
||||||
// Check for duplicate email
|
// 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()}`;
|
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
||||||
|
|
||||||
// Fetch hierarchy from Opportunity if available
|
// Fetch hierarchy from Opportunity if available
|
||||||
|
// Fetch hierarchy from Opportunity if available, OR resolve from Location
|
||||||
let zoneId, regionId, areaId;
|
let zoneId, regionId, areaId;
|
||||||
|
|
||||||
if (opportunityId) {
|
if (opportunityId) {
|
||||||
const opportunity = await Opportunity.findByPk(opportunityId);
|
const opportunity = await Opportunity.findByPk(opportunityId);
|
||||||
if (opportunity) {
|
if (opportunity) {
|
||||||
zoneId = opportunity.zoneId;
|
zoneId = opportunity.zoneId;
|
||||||
regionId = opportunity.regionId;
|
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,
|
state,
|
||||||
experienceYears,
|
experienceYears,
|
||||||
investmentCapacity,
|
investmentCapacity,
|
||||||
currentStage: 'Application',
|
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
|
||||||
overallStatus: 'New',
|
currentStage: APPLICATION_STAGES.DD,
|
||||||
|
overallStatus: APPLICATION_STATUS.PENDING,
|
||||||
progressPercentage: 10,
|
progressPercentage: 10,
|
||||||
zoneId,
|
zoneId,
|
||||||
regionId
|
regionId
|
||||||
@ -56,7 +79,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
await ApplicationStatusHistory.create({
|
await ApplicationStatusHistory.create({
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
previousStatus: null,
|
previousStatus: null,
|
||||||
newStatus: 'New',
|
newStatus: APPLICATION_STATUS.PENDING,
|
||||||
changedBy: req.user?.id,
|
changedBy: req.user?.id,
|
||||||
reason: 'Initial Submission'
|
reason: 'Initial Submission'
|
||||||
});
|
});
|
||||||
@ -65,6 +88,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
await ApplicationProgress.create({
|
await ApplicationProgress.create({
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
stageName: 'Application',
|
stageName: 'Application',
|
||||||
|
stageOrder: 1,
|
||||||
status: 'Completed',
|
status: 'Completed',
|
||||||
completionPercentage: 100
|
completionPercentage: 100
|
||||||
});
|
});
|
||||||
@ -91,7 +115,7 @@ export const getApplications = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
// Add filtering logic here similar to Opportunity
|
// Add filtering logic here similar to Opportunity
|
||||||
const applications = await Application.findAll({
|
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']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -115,7 +139,7 @@ export const getApplicationById = async (req: Request, res: Response) => {
|
|||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{ model: ApplicationStatusHistory, as: 'statusHistory' },
|
{ 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';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
|
||||||
// All routes require authentication (or public for submission? Keeping auth for now)
|
// 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.use(authenticate as any);
|
||||||
|
|
||||||
router.post('/apply', onboardingController.submitApplication);
|
|
||||||
router.get('/applications', onboardingController.getApplications);
|
router.get('/applications', onboardingController.getApplications);
|
||||||
router.get('/applications/:id', onboardingController.getApplicationById);
|
router.get('/applications/:id', onboardingController.getApplicationById);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
||||||
|
|||||||
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 dealerRoutes from './modules/dealer/dealer.routes.js';
|
||||||
import slaRoutes from './modules/sla/sla.routes.js';
|
import slaRoutes from './modules/sla/sla.routes.js';
|
||||||
import communicationRoutes from './modules/communication/communication.routes.js';
|
import communicationRoutes from './modules/communication/communication.routes.js';
|
||||||
|
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
||||||
|
|
||||||
// Import common middleware & utils
|
// Import common middleware & utils
|
||||||
import errorHandler from './common/middleware/errorHandler.js';
|
import errorHandler from './common/middleware/errorHandler.js';
|
||||||
@ -52,7 +53,7 @@ app.use(cors({
|
|||||||
// Rate limiting
|
// Rate limiting
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
|
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.'
|
message: 'Too many requests from this IP, please try again later.'
|
||||||
});
|
});
|
||||||
app.use('/api/', limiter);
|
app.use('/api/', limiter);
|
||||||
@ -104,6 +105,7 @@ app.use('/api/eor', eorRoutes);
|
|||||||
app.use('/api/dealer', dealerRoutes);
|
app.use('/api/dealer', dealerRoutes);
|
||||||
app.use('/api/sla', slaRoutes);
|
app.use('/api/sla', slaRoutes);
|
||||||
app.use('/api/communication', communicationRoutes);
|
app.use('/api/communication', communicationRoutes);
|
||||||
|
app.use('/api/questionnaire', questionnaireRoutes);
|
||||||
|
|
||||||
// Backward Compatibility Aliases
|
// Backward Compatibility Aliases
|
||||||
app.use('/api/applications', onboardingRoutes);
|
app.use('/api/applications', onboardingRoutes);
|
||||||
@ -144,7 +146,7 @@ const startServer = async () => {
|
|||||||
|
|
||||||
// Sync database (in development only)
|
// Sync database (in development only)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
await db.sequelize.sync({ alter: false });
|
await db.sequelize.sync({ alter: true });
|
||||||
logger.info('Database models synchronized');
|
logger.info('Database models synchronized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user