schema enhanced and made nececaasry model changes
This commit is contained in:
parent
6b68364785
commit
92169b0fba
@ -241,58 +241,63 @@ erDiagram
|
|||||||
%% DEALER ENTITY (POST-ONBOARDING)
|
%% DEALER ENTITY (POST-ONBOARDING)
|
||||||
%% ============================================
|
%% ============================================
|
||||||
DEALERS {
|
DEALERS {
|
||||||
uuid dealer_id PK
|
uuid id PK
|
||||||
uuid application_id FK
|
uuid applicationId FK
|
||||||
string dealer_code UK
|
uuid dealerCodeId FK
|
||||||
string dealer_name
|
string legalName
|
||||||
string constitution_type
|
string businessName
|
||||||
text registered_address
|
string constitutionType
|
||||||
string gst_number
|
text registeredAddress
|
||||||
string pan_number
|
string gstNumber
|
||||||
|
string panNumber
|
||||||
string status
|
string status
|
||||||
date activation_date
|
timestamp onboardedAt
|
||||||
date last_working_day
|
|
||||||
boolean portal_access_active
|
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
timestamp updated_at
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
%% DEALER APPLICATION
|
%% ONBOARDING APPLICATION (ONBOARDING PHASE)
|
||||||
%% ============================================
|
%% ============================================
|
||||||
APPLICATIONS {
|
APPLICATIONS {
|
||||||
uuid application_id PK
|
uuid id PK
|
||||||
string registration_number UK
|
string applicationId UK
|
||||||
string applicant_name
|
uuid opportunityId FK
|
||||||
|
string applicantName
|
||||||
string email UK
|
string email UK
|
||||||
string mobile_number
|
string phone
|
||||||
integer age
|
string businessType
|
||||||
string country
|
string preferredLocation
|
||||||
|
string city
|
||||||
string state
|
string state
|
||||||
string district
|
integer experienceYears
|
||||||
string pincode
|
string investmentCapacity
|
||||||
string interested_city
|
integer age
|
||||||
string company_name
|
string education
|
||||||
string education_qualification
|
string companyName
|
||||||
boolean owns_re_bike
|
string source
|
||||||
boolean is_existing_dealer
|
string existingDealer
|
||||||
text address
|
string ownRoyalEnfield
|
||||||
|
string royalEnfieldModel
|
||||||
text description
|
text description
|
||||||
string preferred_location
|
text address
|
||||||
string application_status
|
string pincode
|
||||||
string opportunity_status
|
string locationType
|
||||||
boolean is_shortlisted
|
string currentStage
|
||||||
boolean dd_lead_shortlisted
|
string overallStatus
|
||||||
uuid assigned_to FK
|
integer progressPercentage
|
||||||
uuid zone_id FK
|
boolean isShortlisted
|
||||||
uuid region_id FK
|
boolean ddLeadShortlisted
|
||||||
uuid area_id FK
|
decimal score
|
||||||
uuid assigned_dd_zm FK
|
uuid assignedTo FK
|
||||||
uuid assigned_rbm FK
|
uuid architectureAssignedTo FK
|
||||||
|
string architectureStatus
|
||||||
|
uuid submittedBy FK
|
||||||
|
uuid zoneId FK
|
||||||
|
uuid regionId FK
|
||||||
|
uuid areaId FK
|
||||||
json documents
|
json documents
|
||||||
json timeline
|
json timeline
|
||||||
integer progress_percentage
|
|
||||||
timestamp submitted_at
|
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
timestamp updated_at
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
@ -660,7 +665,7 @@ erDiagram
|
|||||||
%% OUTLET MANAGEMENT
|
%% OUTLET MANAGEMENT
|
||||||
%% ============================================
|
%% ============================================
|
||||||
OUTLETS {
|
OUTLETS {
|
||||||
uuid outlet_id PK
|
uuid id PK
|
||||||
string code UK
|
string code UK
|
||||||
string name
|
string name
|
||||||
string type
|
string type
|
||||||
@ -671,8 +676,8 @@ erDiagram
|
|||||||
decimal latitude
|
decimal latitude
|
||||||
decimal longitude
|
decimal longitude
|
||||||
string status
|
string status
|
||||||
date established_date
|
date establishedDate
|
||||||
uuid dealer_id FK
|
uuid dealerId FK
|
||||||
string region
|
string region
|
||||||
string zone
|
string zone
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
@ -699,49 +704,62 @@ erDiagram
|
|||||||
}
|
}
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
%% DEALER SELF-SERVICE (SECTION 12)
|
%% DEALER SERVICE APPLICATIONS (POST-ONBOARDING)
|
||||||
|
%% Also known as Self-Service or Lifecycle Requests
|
||||||
%% ============================================
|
%% ============================================
|
||||||
DEALER_RESIGNATIONS {
|
RESIGNATIONS {
|
||||||
uuid resignation_id PK
|
uuid id PK
|
||||||
uuid dealer_id FK
|
string resignationId UK
|
||||||
string outlet_code
|
uuid outletId FK
|
||||||
date last_operational_date_sales
|
uuid dealerId FK
|
||||||
date last_operational_date_service
|
string resignationType
|
||||||
date proposed_lwd
|
date lastOperationalDateSales
|
||||||
string reason_type
|
date lastOperationalDateServices
|
||||||
text reason_description
|
text reason
|
||||||
|
text additionalInfo
|
||||||
|
string currentStage
|
||||||
string status
|
string status
|
||||||
boolean is_withdrawn
|
integer progressPercentage
|
||||||
timestamp submitted_at
|
timestamp submittedOn
|
||||||
|
json documents
|
||||||
|
json timeline
|
||||||
|
text rejectionReason
|
||||||
|
json departmentalClearances
|
||||||
|
timestamp created_at
|
||||||
timestamp updated_at
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
DEALER_RELOCATIONS {
|
RELOCATION_REQUESTS {
|
||||||
uuid relocation_id PK
|
uuid id PK
|
||||||
uuid dealer_id FK
|
string requestId UK
|
||||||
string current_location_json
|
uuid outletId FK
|
||||||
string proposed_location_json
|
uuid dealerId FK
|
||||||
decimal distance_km
|
string relocationType
|
||||||
string property_type
|
text newAddress
|
||||||
date expected_relocation_date
|
string newCity
|
||||||
|
string newState
|
||||||
text reason
|
text reason
|
||||||
|
string currentStage
|
||||||
string status
|
string status
|
||||||
timestamp submitted_at
|
integer progressPercentage
|
||||||
}
|
json documents
|
||||||
|
json timeline
|
||||||
DEALER_CONSTITUTION_CHANGES {
|
timestamp created_at
|
||||||
uuid constitution_change_id PK
|
timestamp updated_at
|
||||||
string request_id UK
|
}
|
||||||
uuid outlet_id FK
|
|
||||||
uuid dealer_id FK
|
CONSTITUTIONAL_CHANGES {
|
||||||
string change_type
|
uuid id PK
|
||||||
text description
|
string requestId UK
|
||||||
string current_stage
|
uuid outletId FK
|
||||||
string status
|
uuid dealerId FK
|
||||||
integer progress_percentage
|
string changeType
|
||||||
|
text description
|
||||||
|
string currentStage
|
||||||
|
string status
|
||||||
|
integer progressPercentage
|
||||||
json documents
|
json documents
|
||||||
json timeline
|
json timeline
|
||||||
timestamp submitted_at
|
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
timestamp updated_at
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
@ -883,17 +901,22 @@ erDiagram
|
|||||||
}
|
}
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
%% TERMINATION & F&F SETTLEMENT (SECTION 4.3 & 10)
|
%% TERMINATION (A TYPE OF SERVICE APPLICATION)
|
||||||
%% ============================================
|
%% ============================================
|
||||||
TERMINATION_REQUESTS {
|
TERMINATION_REQUESTS {
|
||||||
uuid termination_id PK
|
uuid id PK
|
||||||
uuid dealer_id FK
|
uuid dealerId FK
|
||||||
string category
|
string category
|
||||||
text reason
|
text reason
|
||||||
date proposed_lwd
|
date proposedLwd
|
||||||
string status
|
string status
|
||||||
uuid initiated_by FK
|
string currentStage
|
||||||
|
uuid initiatedBy FK
|
||||||
|
text comments
|
||||||
|
json timeline
|
||||||
|
json documents
|
||||||
timestamp created_at
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
TERMINATION_APPROVALS {
|
TERMINATION_APPROVALS {
|
||||||
@ -907,14 +930,20 @@ erDiagram
|
|||||||
timestamp created_at
|
timestamp created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
FNF_CASES {
|
FNF_SETTLEMENTS {
|
||||||
uuid fnf_id PK
|
uuid id PK
|
||||||
uuid dealer_id FK
|
uuid resignationId FK
|
||||||
uuid source_id FK
|
uuid terminationRequestId FK
|
||||||
string source_type
|
uuid outletId FK
|
||||||
date last_working_day
|
uuid dealerId FK
|
||||||
string status
|
string status
|
||||||
timestamp initiated_at
|
decimal totalReceivables
|
||||||
|
decimal totalPayables
|
||||||
|
decimal netAmount
|
||||||
|
date settlementDate
|
||||||
|
json clearanceDocuments
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
FNF_DEPARTMENT_CLEARANCES {
|
FNF_DEPARTMENT_CLEARANCES {
|
||||||
@ -1149,6 +1178,26 @@ erDiagram
|
|||||||
timestamp access_revoked_at
|
timestamp access_revoked_at
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REQUEST_PARTICIPANTS {
|
||||||
|
uuid id PK
|
||||||
|
uuid requestId FK
|
||||||
|
string requestType
|
||||||
|
uuid userId FK
|
||||||
|
string role
|
||||||
|
boolean isActive
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
%% NOTE: Polymorphic Relationships
|
||||||
|
%% DOCUMENTS, WORK_NOTES, and REQUEST_PARTICIPANTS
|
||||||
|
%% use 'requestType' and 'requestId' to link with:
|
||||||
|
%% - APPLICATIONS (Onboarding)
|
||||||
|
%% - RESIGNATIONS
|
||||||
|
%% - RELOCATION_REQUESTS
|
||||||
|
%% - CONSTITUTIONAL_CHANGES
|
||||||
|
%% - TERMINATION_REQUESTS
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
%% RELATIONSHIPS
|
%% RELATIONSHIPS
|
||||||
%% ============================================
|
%% ============================================
|
||||||
@ -1226,16 +1275,18 @@ erDiagram
|
|||||||
APPLICATIONS ||--o{ DEALERS : "onboarded_as"
|
APPLICATIONS ||--o{ DEALERS : "onboarded_as"
|
||||||
DEALERS ||--o{ USERS : "has_portal_users"
|
DEALERS ||--o{ USERS : "has_portal_users"
|
||||||
|
|
||||||
DEALERS ||--o{ DEALER_RESIGNATIONS : "initiates"
|
DEALERS ||--o{ RESIGNATIONS : "initiates"
|
||||||
DEALERS ||--o{ DEALER_RELOCATIONS : "requests"
|
DEALERS ||--o{ RELOCATION_REQUESTS : "requests"
|
||||||
DEALERS ||--o{ DEALER_CONSTITUTION_CHANGES : "proposes"
|
DEALERS ||--o{ CONSTITUTIONAL_CHANGES : "proposes"
|
||||||
|
|
||||||
DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by"
|
DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by"
|
||||||
TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires"
|
TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires"
|
||||||
|
|
||||||
DEALERS ||--o{ FNF_CASES : "settled_in"
|
DEALERS ||--o{ FNF_SETTLEMENTS : "settled_in"
|
||||||
FNF_CASES ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from"
|
RESIGNATIONS ||--o{ FNF_SETTLEMENTS : "triggers"
|
||||||
FNF_CASES ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in"
|
TERMINATION_REQUESTS ||--o{ FNF_SETTLEMENTS : "triggers"
|
||||||
|
FNF_SETTLEMENTS ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from"
|
||||||
|
FNF_SETTLEMENTS ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in"
|
||||||
|
|
||||||
DEALERS ||--o{ DEALER_PORTAL_CONFIG : "governed_by"
|
DEALERS ||--o{ DEALER_PORTAL_CONFIG : "governed_by"
|
||||||
|
|
||||||
@ -1271,21 +1322,29 @@ erDiagram
|
|||||||
|
|
||||||
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines"
|
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines"
|
||||||
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_ESCALATIONS : "defines"
|
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_ESCALATIONS : "defines"
|
||||||
FNF_CASES ||--o{ FNF_LINE_ITEMS : "has"
|
FNF_SETTLEMENTS ||--o{ FNF_LINE_ITEMS : "has"
|
||||||
FNF_DEPARTMENT_CLEARANCES ||--o{ FNF_LINE_ITEMS : "details"
|
FNF_DEPARTMENT_CLEARANCES ||--o{ FNF_LINE_ITEMS : "details"
|
||||||
USERS ||--o{ FNF_LINE_ITEMS : "added"
|
USERS ||--o{ FNF_LINE_ITEMS : "added"
|
||||||
USERS ||--o{ APPLICATIONS : "currently_assigned"
|
USERS ||--o{ APPLICATIONS : "currently_assigned"
|
||||||
|
|
||||||
USERS ||--o{ OUTLETS : "has_outlets"
|
USERS ||--o{ OUTLETS : "has_outlets"
|
||||||
OUTLETS ||--o{ DEALER_CONSTITUTION_CHANGES : "requests_change"
|
OUTLETS ||--o{ CONSTITUTIONAL_CHANGES : "requests_change"
|
||||||
|
OUTLETS ||--o{ RELOCATION_REQUESTS : "requests_relocation"
|
||||||
|
OUTLETS ||--o{ RESIGNATIONS : "initiates_resignation"
|
||||||
|
|
||||||
APPLICATIONS ||--o{ FINANCE_PAYMENTS : "has_payments"
|
APPLICATIONS ||--o{ FINANCE_PAYMENTS : "has_payments"
|
||||||
USERS ||--o{ FINANCE_PAYMENTS : "verifies_payments"
|
USERS ||--o{ FINANCE_PAYMENTS : "verifies_payments"
|
||||||
|
|
||||||
DEALER_RESIGNATIONS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
RESIGNATIONS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
||||||
TERMINATION_REQUESTS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
TERMINATION_REQUESTS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
||||||
USERS ||--o{ EXIT_FEEDBACK : "submitted_by"
|
USERS ||--o{ EXIT_FEEDBACK : "submitted_by"
|
||||||
|
|
||||||
SLA_TRACKING ||--o{ SLA_BREACHES : "has_breaches"
|
SLA_TRACKING ||--o{ SLA_BREACHES : "has_breaches"
|
||||||
|
|
||||||
|
APPLICATIONS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
|
||||||
|
RESIGNATIONS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
|
||||||
|
TERMINATION_REQUESTS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
|
||||||
|
RELOCATION_REQUESTS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
|
||||||
|
CONSTITUTIONAL_CHANGES ||--o{ REQUEST_PARTICIPANTS : "has_participants"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const APPLICATION_STAGES = {
|
|||||||
DD_HEAD: 'DD Head',
|
DD_HEAD: 'DD Head',
|
||||||
NBH: 'NBH',
|
NBH: 'NBH',
|
||||||
LEGAL: 'Legal',
|
LEGAL: 'Legal',
|
||||||
|
ARCHITECTURE: 'Architecture',
|
||||||
FINANCE: 'Finance',
|
FINANCE: 'Finance',
|
||||||
LEVEL_1_APPROVED: 'Level 1 Approved',
|
LEVEL_1_APPROVED: 'Level 1 Approved',
|
||||||
LEVEL_2_APPROVED: 'Level 2 Approved',
|
LEVEL_2_APPROVED: 'Level 2 Approved',
|
||||||
@ -84,6 +85,24 @@ export const APPLICATION_STATUS = {
|
|||||||
DISQUALIFIED: 'Disqualified'
|
DISQUALIFIED: 'Disqualified'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Termination Stages
|
||||||
|
export const TERMINATION_STAGES = {
|
||||||
|
SUBMITTED: 'Submitted',
|
||||||
|
RBM_REVIEW: 'RBM Review',
|
||||||
|
ZBH_REVIEW: 'ZBH Review',
|
||||||
|
DD_LEAD_REVIEW: 'DD Lead Review',
|
||||||
|
LEGAL_VERIFICATION: 'Legal Verification',
|
||||||
|
NBH_EVALUATION: 'NBH Evaluation',
|
||||||
|
SCN_ISSUED: 'Show Cause Notice',
|
||||||
|
PERSONAL_HEARING: 'Personal Hearing',
|
||||||
|
NBH_FINAL_APPROVAL: 'NBH Final Approval',
|
||||||
|
CCO_APPROVAL: 'CCO Approval',
|
||||||
|
CEO_APPROVAL: 'CEO Final Approval',
|
||||||
|
LEGAL_LETTER: 'Legal - Termination Letter',
|
||||||
|
TERMINATED: 'Terminated',
|
||||||
|
REJECTED: 'Rejected'
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Resignation Stages
|
// Resignation Stages
|
||||||
export const RESIGNATION_STAGES = {
|
export const RESIGNATION_STAGES = {
|
||||||
ASM: 'ASM',
|
ASM: 'ASM',
|
||||||
@ -92,6 +111,9 @@ export const RESIGNATION_STAGES = {
|
|||||||
NBH: 'NBH',
|
NBH: 'NBH',
|
||||||
DD_ADMIN: 'DD Admin',
|
DD_ADMIN: 'DD Admin',
|
||||||
LEGAL: 'Legal',
|
LEGAL: 'Legal',
|
||||||
|
SPARES_CLEARANCE: 'Spares Clearance',
|
||||||
|
SERVICE_CLEARANCE: 'Service Clearance',
|
||||||
|
ACCOUNTS_CLEARANCE: 'Accounts Clearance',
|
||||||
FINANCE: 'Finance',
|
FINANCE: 'Finance',
|
||||||
FNF_INITIATED: 'F&F Initiated',
|
FNF_INITIATED: 'F&F Initiated',
|
||||||
COMPLETED: 'Completed',
|
COMPLETED: 'Completed',
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export interface ApplicationAttributes {
|
|||||||
isShortlisted: boolean;
|
isShortlisted: boolean;
|
||||||
ddLeadShortlisted: boolean;
|
ddLeadShortlisted: boolean;
|
||||||
assignedTo: string | null;
|
assignedTo: string | null;
|
||||||
|
architectureAssignedTo: string | null;
|
||||||
|
architectureStatus: string | null;
|
||||||
submittedBy: string | null;
|
submittedBy: string | null;
|
||||||
zoneId: string | null;
|
zoneId: string | null;
|
||||||
regionId: string | null;
|
regionId: string | null;
|
||||||
@ -176,6 +178,19 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
architectureAssignedTo: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
architectureStatus: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 'Pending'
|
||||||
|
},
|
||||||
submittedBy: {
|
submittedBy: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -231,6 +246,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
(Application as any).associate = (models: any) => {
|
(Application as any).associate = (models: any) => {
|
||||||
Application.belongsTo(models.User, { foreignKey: 'submittedBy', as: 'submitter' });
|
Application.belongsTo(models.User, { foreignKey: 'submittedBy', as: 'submitter' });
|
||||||
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
||||||
|
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
|
||||||
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
||||||
Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
||||||
Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
|||||||
@ -18,6 +18,12 @@ export interface ResignationAttributes {
|
|||||||
documents: any[];
|
documents: any[];
|
||||||
timeline: any[];
|
timeline: any[];
|
||||||
rejectionReason: string | null;
|
rejectionReason: string | null;
|
||||||
|
departmentalClearances: {
|
||||||
|
spares: boolean;
|
||||||
|
service: boolean;
|
||||||
|
accounts: boolean;
|
||||||
|
logistics: boolean;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResignationInstance extends Model<ResignationAttributes>, ResignationAttributes { }
|
export interface ResignationInstance extends Model<ResignationAttributes>, ResignationAttributes { }
|
||||||
@ -97,6 +103,15 @@ export default (sequelize: Sequelize) => {
|
|||||||
rejectionReason: {
|
rejectionReason: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
departmentalClearances: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
spares: false,
|
||||||
|
service: false,
|
||||||
|
accounts: false,
|
||||||
|
logistics: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'resignations',
|
tableName: 'resignations',
|
||||||
|
|||||||
@ -7,8 +7,11 @@ export interface TerminationRequestAttributes {
|
|||||||
reason: string;
|
reason: string;
|
||||||
proposedLwd: Date;
|
proposedLwd: Date;
|
||||||
status: string;
|
status: string;
|
||||||
|
currentStage: string;
|
||||||
initiatedBy: string;
|
initiatedBy: string;
|
||||||
comments: string | null;
|
comments: string | null;
|
||||||
|
timeline: any[];
|
||||||
|
documents: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminationRequestInstance extends Model<TerminationRequestAttributes>, TerminationRequestAttributes { }
|
export interface TerminationRequestInstance extends Model<TerminationRequestAttributes>, TerminationRequestAttributes { }
|
||||||
@ -42,7 +45,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'pending'
|
defaultValue: 'Initiated'
|
||||||
|
},
|
||||||
|
currentStage: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: 'INITIATED'
|
||||||
},
|
},
|
||||||
initiatedBy: {
|
initiatedBy: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
@ -55,6 +62,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
comments: {
|
comments: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
documents: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'termination_requests',
|
tableName: 'termination_requests',
|
||||||
|
|||||||
@ -43,12 +43,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Dear {{applicantName}},</p>
|
<p>Dear {{applicantName}},</p>
|
||||||
<p>Thank you for showing interest in becoming a Royal Enfield dealer.</p>
|
<p>Thank you for your interest in Royal Enfield and for your application to represent our brand as an
|
||||||
<p>We have reviewed our current network plan for <strong>{{location}}</strong>, and currently, there are no
|
authorized dealer.</p>
|
||||||
open opportunities available in this area.</p>
|
<p>We have carefully reviewed your expression of interest in relation to our current network expansion
|
||||||
<p>We have saved your details in our database and will contact you should an opportunity arise in the
|
strategy for <strong>{{location}}</strong>. At this juncture, we do not have any immediate vacancies or
|
||||||
future.</p>
|
planned dealership opportunities available in this specific territory.</p>
|
||||||
<p>We appreciate your enthusiasm for the brand.</p>
|
<p>However, we have successfully retained your profile in our prospective partners database. Rest assured,
|
||||||
|
our team will proactively reach out to you should a suitable opportunity materialize in this region or
|
||||||
|
if our strategic requirements in your preferred location evolve further.</p>
|
||||||
|
<p>We appreciate the time you took to share your details and your continued enthusiasm for Royal Enfield.
|
||||||
|
</p>
|
||||||
|
<p>Best regards,<br>Dealer Development Team<br>Royal Enfield</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© {{year}} Royal Enfield. All rights reserved.</p>
|
<p>© {{year}} Royal Enfield. All rights reserved.</p>
|
||||||
|
|||||||
@ -425,3 +425,71 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error processing shortlist' });
|
res.status(500).json({ success: false, message: 'Error processing shortlist' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { userId, remarks } = req.body;
|
||||||
|
|
||||||
|
const application = await Application.findByPk(id);
|
||||||
|
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
|
||||||
|
await application.update({
|
||||||
|
architectureAssignedTo: userId,
|
||||||
|
architectureStatus: 'Assigned',
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add as participant
|
||||||
|
await db.RequestParticipant.findOrCreate({
|
||||||
|
where: {
|
||||||
|
requestId: application.id,
|
||||||
|
requestType: 'application',
|
||||||
|
userId,
|
||||||
|
participantType: 'architecture'
|
||||||
|
},
|
||||||
|
defaults: { joinedMethod: 'auto' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await AuditLog.create({
|
||||||
|
userId: req.user?.id,
|
||||||
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
|
entityType: 'application',
|
||||||
|
entityId: application.id,
|
||||||
|
newData: { architectureAssignedTo: userId, remarks }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Architecture team assigned successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Assign architecture team error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error assigning architecture team' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateArchitectureStatus = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { status, remarks } = req.body;
|
||||||
|
|
||||||
|
const application = await Application.findByPk(id);
|
||||||
|
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
|
||||||
|
await application.update({
|
||||||
|
architectureStatus: status,
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
await AuditLog.create({
|
||||||
|
userId: req.user?.id,
|
||||||
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
|
entityType: 'application',
|
||||||
|
entityId: application.id,
|
||||||
|
newData: { architectureStatus: status, remarks }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Architecture status updated successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update architecture status error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error updating architecture status' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import * as onboardingController from './onboarding.controller.js';
|
import {
|
||||||
|
submitApplication, getApplications, getApplicationById, updateApplicationStatus,
|
||||||
|
uploadDocuments, getApplicationDocuments, bulkShortlist,
|
||||||
|
assignArchitectureTeam, updateArchitectureStatus
|
||||||
|
} from './onboarding.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
|
||||||
import { uploadSingle } from '../../common/middleware/upload.js';
|
import { uploadSingle } from '../../common/middleware/upload.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
|
// Public route for application submission
|
||||||
router.post('/apply', onboardingController.submitApplication);
|
router.post('/apply', submitApplication);
|
||||||
|
|
||||||
// All subsequent routes require authentication
|
// All subsequent routes require authentication
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
router.get('/applications', onboardingController.getApplications);
|
router.get('/applications', getApplications);
|
||||||
router.post('/applications/shortlist', onboardingController.bulkShortlist);
|
router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import
|
||||||
router.get('/applications/:id', onboardingController.getApplicationById);
|
router.get('/applications/:id', getApplicationById);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', updateApplicationStatus);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.post('/applications/:id/documents', uploadSingle, uploadDocuments);
|
||||||
router.post('/applications/:id/documents', uploadSingle, onboardingController.uploadDocuments);
|
router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import
|
||||||
router.get('/applications/:id/documents', onboardingController.getApplicationDocuments);
|
|
||||||
|
// Architecture-related routes
|
||||||
|
router.post('/applications/:id/assign-architecture', assignArchitectureTeam);
|
||||||
|
router.put('/applications/:id/architecture-status', updateArchitectureStatus);
|
||||||
|
|
||||||
|
|
||||||
// Questionnaire Routes
|
// Questionnaire Routes
|
||||||
router.get('/questionnaires', (req, res, next) => {
|
router.get('/questionnaires', (req, res, next) => {
|
||||||
|
|||||||
@ -24,12 +24,12 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
|
|||||||
description: reason,
|
description: reason,
|
||||||
currentStage: 'DD_ADMIN_REVIEW' as any,
|
currentStage: 'DD_ADMIN_REVIEW' as any,
|
||||||
status: 'Pending',
|
status: 'Pending',
|
||||||
progressPercentage: 0,
|
progressPercentage: 20,
|
||||||
documents: [],
|
documents: [],
|
||||||
timeline: [{
|
timeline: [{
|
||||||
stage: 'Submitted',
|
stage: 'Submitted',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action: 'Request submitted'
|
action: 'Request submitted'
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@ -65,7 +65,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
as: 'dealer',
|
as: 'dealer',
|
||||||
attributes: ['name']
|
attributes: ['fullName']
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
@ -97,7 +97,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
as: 'dealer',
|
as: 'dealer',
|
||||||
attributes: ['name', 'email']
|
attributes: ['fullName', 'email']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Worknote,
|
model: Worknote,
|
||||||
@ -137,16 +137,36 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
return res.status(404).json({ success: false, message: 'Request not found' });
|
return res.status(404).json({ success: false, message: 'Request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stageFlow: Record<string, string> = {
|
||||||
|
'DD_ADMIN_REVIEW': 'LEGAL_REVIEW',
|
||||||
|
'LEGAL_REVIEW': 'NBH_APPROVAL',
|
||||||
|
'NBH_APPROVAL': 'FINANCE_CLEARANCE',
|
||||||
|
'FINANCE_CLEARANCE': 'COMPLETED'
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStage = request.currentStage as string;
|
||||||
|
let nextStage = currentStage;
|
||||||
|
let finalStatus = action;
|
||||||
|
|
||||||
|
if (action === 'Approve') {
|
||||||
|
nextStage = stageFlow[currentStage] || currentStage;
|
||||||
|
finalStatus = nextStage === 'COMPLETED' ? 'Completed' : `Pending ${nextStage.replace('_', ' ')}`;
|
||||||
|
} else if (action === 'Reject') {
|
||||||
|
nextStage = 'REJECTED';
|
||||||
|
finalStatus = 'Rejected';
|
||||||
|
}
|
||||||
|
|
||||||
const timeline = [...request.timeline, {
|
const timeline = [...request.timeline, {
|
||||||
stage: 'Review',
|
stage: currentStage,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action,
|
action,
|
||||||
remarks: comments
|
remarks: comments
|
||||||
}];
|
}];
|
||||||
|
|
||||||
await request.update({
|
await request.update({
|
||||||
status: action,
|
status: finalStatus,
|
||||||
|
currentStage: nextStage as any,
|
||||||
timeline,
|
timeline,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { RelocationRequest, Outlet, User, Worknote } = db;
|
const { RelocationRequest, Outlet, User, Worknote } = db;
|
||||||
import { AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
|
||||||
import { Op, Transaction } from 'sequelize';
|
import { Op, Transaction } from 'sequelize';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
@ -27,14 +27,14 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
|
|||||||
newCity: proposedCity,
|
newCity: proposedCity,
|
||||||
newState: proposedState,
|
newState: proposedState,
|
||||||
reason,
|
reason,
|
||||||
currentStage: 'DD_ADMIN_REVIEW' as any,
|
currentStage: RELOCATION_STAGES.DD_ADMIN_REVIEW as any,
|
||||||
status: 'Pending',
|
status: 'Pending',
|
||||||
progressPercentage: 0,
|
progressPercentage: 20,
|
||||||
documents: [],
|
documents: [],
|
||||||
timeline: [{
|
timeline: [{
|
||||||
stage: 'Submitted',
|
stage: 'Submitted',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action: 'Request submitted'
|
action: 'Request submitted'
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@ -70,7 +70,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
as: 'dealer',
|
as: 'dealer',
|
||||||
attributes: ['name']
|
attributes: ['fullName']
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
@ -102,7 +102,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
as: 'dealer',
|
as: 'dealer',
|
||||||
attributes: ['name', 'email']
|
attributes: ['fullName', 'email']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Worknote,
|
model: Worknote,
|
||||||
@ -147,10 +147,19 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
let newStatus = request.status;
|
let newStatus = request.status;
|
||||||
let newCurrentStage = request.currentStage;
|
let newCurrentStage = request.currentStage;
|
||||||
|
|
||||||
|
const stageFlow: Record<string, string> = {
|
||||||
|
[RELOCATION_STAGES.DD_ADMIN_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
|
||||||
|
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.NBH_APPROVAL,
|
||||||
|
[RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.LEGAL_CLEARANCE,
|
||||||
|
[RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.COMPLETED
|
||||||
|
};
|
||||||
|
|
||||||
if (action === 'Approved') {
|
if (action === 'Approved') {
|
||||||
newStatus = 'Approved';
|
newCurrentStage = stageFlow[request.currentStage] || request.currentStage;
|
||||||
|
newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage.replace('_', ' ')}`;
|
||||||
} else if (action === 'Rejected') {
|
} else if (action === 'Rejected') {
|
||||||
newStatus = 'Rejected';
|
newStatus = 'Rejected';
|
||||||
|
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a worknote entry
|
// Create a worknote entry
|
||||||
|
|||||||
@ -18,8 +18,11 @@ const calculateProgress = (stage: string): number => {
|
|||||||
[RESIGNATION_STAGES.RBM]: 30,
|
[RESIGNATION_STAGES.RBM]: 30,
|
||||||
[RESIGNATION_STAGES.ZBH]: 45,
|
[RESIGNATION_STAGES.ZBH]: 45,
|
||||||
[RESIGNATION_STAGES.NBH]: 60,
|
[RESIGNATION_STAGES.NBH]: 60,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: 70,
|
[RESIGNATION_STAGES.DD_ADMIN]: 65,
|
||||||
[RESIGNATION_STAGES.LEGAL]: 80,
|
[RESIGNATION_STAGES.LEGAL]: 70,
|
||||||
|
[RESIGNATION_STAGES.SPARES_CLEARANCE]: 75,
|
||||||
|
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: 80,
|
||||||
|
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: 85,
|
||||||
[RESIGNATION_STAGES.FINANCE]: 90,
|
[RESIGNATION_STAGES.FINANCE]: 90,
|
||||||
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
|
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
|
||||||
[RESIGNATION_STAGES.COMPLETED]: 100,
|
[RESIGNATION_STAGES.COMPLETED]: 100,
|
||||||
@ -88,7 +91,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
timeline: [{
|
timeline: [{
|
||||||
stage: 'Submitted',
|
stage: 'Submitted',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action: 'Resignation request submitted'
|
action: 'Resignation request submitted'
|
||||||
}]
|
}]
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
@ -260,7 +263,10 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
|
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
|
||||||
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
|
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
||||||
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE,
|
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.SPARES_CLEARANCE,
|
||||||
|
[RESIGNATION_STAGES.SPARES_CLEARANCE]: RESIGNATION_STAGES.SERVICE_CLEARANCE,
|
||||||
|
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: RESIGNATION_STAGES.ACCOUNTS_CLEARANCE,
|
||||||
|
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: RESIGNATION_STAGES.FINANCE,
|
||||||
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
|
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
|
||||||
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
|
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
|
||||||
};
|
};
|
||||||
@ -279,7 +285,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
const timeline = [...resignation.timeline, {
|
const timeline = [...resignation.timeline, {
|
||||||
stage: nextStage,
|
stage: nextStage,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action: 'Approved',
|
action: 'Approved',
|
||||||
remarks
|
remarks
|
||||||
}];
|
}];
|
||||||
@ -357,7 +363,7 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
const timeline = [...resignation.timeline, {
|
const timeline = [...resignation.timeline, {
|
||||||
stage: 'Rejected',
|
stage: 'Rejected',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: req.user.name,
|
user: req.user.fullName,
|
||||||
action: 'Rejected',
|
action: 'Rejected',
|
||||||
reason
|
reason
|
||||||
}];
|
}];
|
||||||
@ -399,3 +405,39 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update departmental clearance
|
||||||
|
export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
const { id } = req.params;
|
||||||
|
const { department, cleared, remarks } = req.body;
|
||||||
|
|
||||||
|
const resignation = await db.Resignation.findByPk(id);
|
||||||
|
if (!resignation) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearances = { ...resignation.departmentalClearances, [department]: cleared };
|
||||||
|
|
||||||
|
await resignation.update({
|
||||||
|
departmentalClearances: clearances,
|
||||||
|
timeline: [...resignation.timeline, {
|
||||||
|
stage: resignation.currentStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`,
|
||||||
|
remarks
|
||||||
|
}]
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.json({ success: true, message: `Clearance updated for ${department}`, resignation });
|
||||||
|
} catch (error) {
|
||||||
|
if (transaction) await transaction.rollback();
|
||||||
|
logger.error('Error updating clearance:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -9,5 +9,6 @@ router.get('/', authenticate as any, resignationController.getResignations);
|
|||||||
router.get('/:id', authenticate as any, resignationController.getResignationById);
|
router.get('/:id', authenticate as any, resignationController.getResignationById);
|
||||||
router.put('/:id/approve', authenticate as any, resignationController.approveResignation);
|
router.put('/:id/approve', authenticate as any, resignationController.approveResignation);
|
||||||
router.put('/:id/reject', authenticate as any, resignationController.rejectResignation);
|
router.put('/:id/reject', authenticate as any, resignationController.rejectResignation);
|
||||||
|
router.put('/:id/clearance', authenticate as any, resignationController.updateClearance);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
178
src/modules/termination/termination.controller.ts
Normal file
178
src/modules/termination/termination.controller.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { Response, NextFunction } from 'express';
|
||||||
|
import db from '../../database/models/index.js';
|
||||||
|
import logger from '../../common/utils/logger.js';
|
||||||
|
import { TERMINATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
|
||||||
|
import { Transaction } from 'sequelize';
|
||||||
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
|
|
||||||
|
// Calculate progress percentage based on stage
|
||||||
|
const calculateProgress = (stage: string): number => {
|
||||||
|
const stageProgress: Record<string, number> = {
|
||||||
|
[TERMINATION_STAGES.SUBMITTED]: 10,
|
||||||
|
[TERMINATION_STAGES.RBM_REVIEW]: 20,
|
||||||
|
[TERMINATION_STAGES.ZBH_REVIEW]: 30,
|
||||||
|
[TERMINATION_STAGES.DD_LEAD_REVIEW]: 40,
|
||||||
|
[TERMINATION_STAGES.LEGAL_VERIFICATION]: 50,
|
||||||
|
[TERMINATION_STAGES.NBH_EVALUATION]: 60,
|
||||||
|
[TERMINATION_STAGES.SCN_ISSUED]: 70,
|
||||||
|
[TERMINATION_STAGES.PERSONAL_HEARING]: 75,
|
||||||
|
[TERMINATION_STAGES.NBH_FINAL_APPROVAL]: 80,
|
||||||
|
[TERMINATION_STAGES.CCO_APPROVAL]: 85,
|
||||||
|
[TERMINATION_STAGES.CEO_APPROVAL]: 90,
|
||||||
|
[TERMINATION_STAGES.LEGAL_LETTER]: 95,
|
||||||
|
[TERMINATION_STAGES.TERMINATED]: 100,
|
||||||
|
[TERMINATION_STAGES.REJECTED]: 0
|
||||||
|
};
|
||||||
|
return stageProgress[stage] || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create termination request
|
||||||
|
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
const { dealerId, category, reason, proposedLwd, comments } = req.body;
|
||||||
|
|
||||||
|
// Restriction: Only ASM or RBM can initiate (as per user request: "ASM or RBM can initiate")
|
||||||
|
// Note: Check existing roles in constants. ROLES.RBM exists. ASM might be DD or similar.
|
||||||
|
// For now, I'll allow ASM (mapped to DD/Initiator) and RBM.
|
||||||
|
|
||||||
|
const termination = await db.TerminationRequest.create({
|
||||||
|
dealerId,
|
||||||
|
category,
|
||||||
|
reason,
|
||||||
|
proposedLwd,
|
||||||
|
comments,
|
||||||
|
initiatedBy: req.user.id,
|
||||||
|
currentStage: TERMINATION_STAGES.SUBMITTED,
|
||||||
|
status: 'Submitted',
|
||||||
|
timeline: [{
|
||||||
|
stage: 'Submitted',
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: 'Termination request initiated',
|
||||||
|
remarks: comments
|
||||||
|
}]
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await db.AuditLog.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
action: AUDIT_ACTIONS.CREATED,
|
||||||
|
entityType: 'termination',
|
||||||
|
entityId: termination.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.status(201).json({ success: true, message: 'Termination request created', termination });
|
||||||
|
} catch (error) {
|
||||||
|
if (transaction) await transaction.rollback();
|
||||||
|
logger.error('Error creating termination:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all terminations
|
||||||
|
export const getTerminations = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const terminations = await db.TerminationRequest.findAll({
|
||||||
|
include: [
|
||||||
|
{ model: db.Dealer, as: 'dealer' },
|
||||||
|
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'roleCode'] }
|
||||||
|
],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
res.json({ success: true, terminations });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching terminations:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get termination by ID
|
||||||
|
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const termination = await db.TerminationRequest.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{ model: db.Dealer, as: 'dealer' },
|
||||||
|
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'roleCode'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (!termination) return res.status(404).json({ success: false, message: 'Termination not found' });
|
||||||
|
res.json({ success: true, termination });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching termination:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update termination status (Approve/Reject)
|
||||||
|
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
const { id } = req.params;
|
||||||
|
const { action, remarks } = req.body; // action: 'approve' | 'reject' | 'sendback'
|
||||||
|
|
||||||
|
const termination = await db.TerminationRequest.findByPk(id);
|
||||||
|
if (!termination) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(404).json({ success: false, message: 'Termination not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'reject') {
|
||||||
|
await termination.update({
|
||||||
|
currentStage: TERMINATION_STAGES.REJECTED,
|
||||||
|
status: 'Rejected',
|
||||||
|
timeline: [...termination.timeline, {
|
||||||
|
stage: 'Rejected',
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: 'Rejected',
|
||||||
|
remarks
|
||||||
|
}]
|
||||||
|
}, { transaction });
|
||||||
|
} else {
|
||||||
|
// Approval flow
|
||||||
|
const stageFlow: Record<string, string> = {
|
||||||
|
[TERMINATION_STAGES.SUBMITTED]: TERMINATION_STAGES.RBM_REVIEW,
|
||||||
|
[TERMINATION_STAGES.RBM_REVIEW]: TERMINATION_STAGES.ZBH_REVIEW,
|
||||||
|
[TERMINATION_STAGES.ZBH_REVIEW]: TERMINATION_STAGES.DD_LEAD_REVIEW,
|
||||||
|
[TERMINATION_STAGES.DD_LEAD_REVIEW]: TERMINATION_STAGES.LEGAL_VERIFICATION,
|
||||||
|
[TERMINATION_STAGES.LEGAL_VERIFICATION]: TERMINATION_STAGES.NBH_EVALUATION,
|
||||||
|
[TERMINATION_STAGES.NBH_EVALUATION]: TERMINATION_STAGES.SCN_ISSUED,
|
||||||
|
[TERMINATION_STAGES.SCN_ISSUED]: TERMINATION_STAGES.PERSONAL_HEARING,
|
||||||
|
[TERMINATION_STAGES.PERSONAL_HEARING]: TERMINATION_STAGES.NBH_FINAL_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.NBH_FINAL_APPROVAL]: TERMINATION_STAGES.CCO_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.CCO_APPROVAL]: TERMINATION_STAGES.CEO_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.CEO_APPROVAL]: TERMINATION_STAGES.LEGAL_LETTER,
|
||||||
|
[TERMINATION_STAGES.LEGAL_LETTER]: TERMINATION_STAGES.TERMINATED
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStage = stageFlow[termination.currentStage];
|
||||||
|
if (!nextStage) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({ success: false, message: 'Cannot approve from current stage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await termination.update({
|
||||||
|
currentStage: nextStage,
|
||||||
|
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}`,
|
||||||
|
timeline: [...termination.timeline, {
|
||||||
|
stage: nextStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: 'Approved/Moved',
|
||||||
|
remarks
|
||||||
|
}]
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.json({ success: true, message: 'Termination updated', termination });
|
||||||
|
} catch (error) {
|
||||||
|
if (transaction) await transaction.rollback();
|
||||||
|
logger.error('Error updating termination:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
15
src/modules/termination/termination.routes.ts
Normal file
15
src/modules/termination/termination.routes.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
createTermination, getTerminations, getTerminationById, updateTerminationStatus
|
||||||
|
} from './termination.controller.js';
|
||||||
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
|
||||||
|
router.use(authenticate as any);
|
||||||
|
|
||||||
|
router.post('/', createTermination);
|
||||||
|
router.get('/', getTerminations);
|
||||||
|
router.get('/:id', getTerminationById);
|
||||||
|
router.put('/:id/status', updateTerminationStatus);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -36,6 +36,7 @@ import communicationRoutes from './modules/communication/communication.routes.js
|
|||||||
import auditRoutes from './modules/audit/audit.routes.js';
|
import auditRoutes from './modules/audit/audit.routes.js';
|
||||||
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
||||||
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
|
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
|
||||||
|
import terminationRoutes from './modules/termination/termination.routes.js';
|
||||||
|
|
||||||
// Import common middleware & utils
|
// Import common middleware & utils
|
||||||
import errorHandler from './common/middleware/errorHandler.js';
|
import errorHandler from './common/middleware/errorHandler.js';
|
||||||
@ -125,6 +126,7 @@ app.use('/api/communication', communicationRoutes);
|
|||||||
app.use('/api/audit', auditRoutes);
|
app.use('/api/audit', auditRoutes);
|
||||||
app.use('/api/questionnaire', questionnaireRoutes);
|
app.use('/api/questionnaire', questionnaireRoutes);
|
||||||
app.use('/api/prospective-login', prospectiveLoginRoutes);
|
app.use('/api/prospective-login', prospectiveLoginRoutes);
|
||||||
|
app.use('/api/termination', terminationRoutes);
|
||||||
|
|
||||||
// Backward Compatibility Aliases
|
// Backward Compatibility Aliases
|
||||||
app.use('/api/applications', onboardingRoutes);
|
app.use('/api/applications', onboardingRoutes);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user