seeded data for the state districts andseeded default zones create login api with username password later oka will be added

This commit is contained in:
laxmanhalaki 2026-01-27 19:08:24 +05:30
parent 251a362717
commit f54501793c
71 changed files with 1068 additions and 4119 deletions

View File

@ -1,34 +0,0 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
// Generate JWT token
const generateToken = (user) => {
const payload = {
userId: user.id,
email: user.email,
role: user.role,
region: user.region,
zone: user.zone
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRE
});
};
// Verify JWT token
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
};
module.exports = {
generateToken,
verifyToken,
JWT_SECRET
};

View File

@ -1,209 +0,0 @@
// User Roles
const ROLES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL_ADMIN: 'Legal Admin',
SUPER_ADMIN: 'Super Admin',
DD_AM: 'DD AM',
FINANCE: 'Finance',
DEALER: 'Dealer'
};
// Regions
const REGIONS = {
EAST: 'East',
WEST: 'West',
NORTH: 'North',
SOUTH: 'South',
CENTRAL: 'Central'
};
// Application Stages
const APPLICATION_STAGES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
LEGAL: 'Legal',
FINANCE: 'Finance',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Application Status
const APPLICATION_STATUS = {
PENDING: 'Pending',
IN_REVIEW: 'In Review',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Resignation Stages
const RESIGNATION_STAGES = {
ASM: 'ASM',
RBM: 'RBM',
ZBH: 'ZBH',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL: 'Legal',
FINANCE: 'Finance',
FNF_INITIATED: 'F&F Initiated',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Resignation Types
const RESIGNATION_TYPES = {
VOLUNTARY: 'Voluntary',
RETIREMENT: 'Retirement',
HEALTH_ISSUES: 'Health Issues',
BUSINESS_CLOSURE: 'Business Closure',
OTHER: 'Other'
};
// Constitutional Change Types
const CONSTITUTIONAL_CHANGE_TYPES = {
OWNERSHIP_TRANSFER: 'Ownership Transfer',
PARTNERSHIP_CHANGE: 'Partnership Change',
LLP_CONVERSION: 'LLP Conversion',
COMPANY_FORMATION: 'Company Formation',
DIRECTOR_CHANGE: 'Director Change'
};
// Constitutional Change Stages
const CONSTITUTIONAL_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
LEGAL_REVIEW: 'Legal Review',
NBH_APPROVAL: 'NBH Approval',
FINANCE_CLEARANCE: 'Finance Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Relocation Types
const RELOCATION_TYPES = {
WITHIN_CITY: 'Within City',
INTERCITY: 'Intercity',
INTERSTATE: 'Interstate'
};
// Relocation Stages
const RELOCATION_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
RBM_REVIEW: 'RBM Review',
NBH_APPROVAL: 'NBH Approval',
LEGAL_CLEARANCE: 'Legal Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Outlet Types
const OUTLET_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Outlet Status
const OUTLET_STATUS = {
ACTIVE: 'Active',
PENDING_RESIGNATION: 'Pending Resignation',
CLOSED: 'Closed'
};
// Business Types
const BUSINESS_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Payment Types
const PAYMENT_TYPES = {
SECURITY_DEPOSIT: 'Security Deposit',
LICENSE_FEE: 'License Fee',
SETUP_FEE: 'Setup Fee',
OTHER: 'Other'
};
// Payment Status
const PAYMENT_STATUS = {
PENDING: 'Pending',
PAID: 'Paid',
OVERDUE: 'Overdue',
WAIVED: 'Waived'
};
// F&F Status
const FNF_STATUS = {
INITIATED: 'Initiated',
DD_CLEARANCE: 'DD Clearance',
LEGAL_CLEARANCE: 'Legal Clearance',
FINANCE_APPROVAL: 'Finance Approval',
COMPLETED: 'Completed'
};
// Audit Actions
const AUDIT_ACTIONS = {
CREATED: 'CREATED',
UPDATED: 'UPDATED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
DELETED: 'DELETED',
STAGE_CHANGED: 'STAGE_CHANGED',
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
WORKNOTE_ADDED: 'WORKNOTE_ADDED'
};
// Document Types
const DOCUMENT_TYPES = {
GST_CERTIFICATE: 'GST Certificate',
PAN_CARD: 'PAN Card',
AADHAAR: 'Aadhaar',
PARTNERSHIP_DEED: 'Partnership Deed',
LLP_AGREEMENT: 'LLP Agreement',
INCORPORATION_CERTIFICATE: 'Certificate of Incorporation',
MOA: 'MOA',
AOA: 'AOA',
BOARD_RESOLUTION: 'Board Resolution',
PROPERTY_DOCUMENTS: 'Property Documents',
BANK_STATEMENT: 'Bank Statement',
OTHER: 'Other'
};
// Request Types
const REQUEST_TYPES = {
APPLICATION: 'application',
RESIGNATION: 'resignation',
CONSTITUTIONAL: 'constitutional',
RELOCATION: 'relocation'
};
module.exports = {
ROLES,
REGIONS,
APPLICATION_STAGES,
APPLICATION_STATUS,
RESIGNATION_STAGES,
RESIGNATION_TYPES,
CONSTITUTIONAL_CHANGE_TYPES,
CONSTITUTIONAL_STAGES,
RELOCATION_TYPES,
RELOCATION_STAGES,
OUTLET_TYPES,
OUTLET_STATUS,
BUSINESS_TYPES,
PAYMENT_TYPES,
PAYMENT_STATUS,
FNF_STATUS,
AUDIT_ACTIONS,
DOCUMENT_TYPES,
REQUEST_TYPES
};

View File

@ -1,49 +0,0 @@
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
},
production: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
pool: {
max: 20,
min: 5,
acquire: 60000,
idle: 10000
},
dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? {
require: true,
rejectUnauthorized: false
} : false
}
},
test: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME + '_test' || 'royal_enfield_onboarding_test',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false
}
};

View File

@ -1,12 +0,0 @@
require('dotenv').config();
module.exports = {
host: process.env.EMAIL_HOST || 'smtp.gmail.com',
port: parseInt(process.env.EMAIL_PORT) || 587,
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
from: process.env.EMAIL_FROM || 'Royal Enfield <noreply@royalenfield.com>'
};

View File

@ -1,135 +0,0 @@
const { Application } = require('../models');
const { v4: uuidv4 } = require('uuid');
exports.submitApplication = async (req, res) => {
try {
const {
applicantName, email, phone, businessType, locationType,
preferredLocation, city, state, experienceYears, investmentCapacity
} = req.body;
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
const application = await Application.create({
applicationId,
applicantName,
email,
phone,
businessType,
preferredLocation,
city,
state,
experienceYears,
investmentCapacity,
currentStage: 'DD',
overallStatus: 'Pending',
progressPercentage: 0
});
res.status(201).json({
success: true,
message: 'Application submitted successfully',
applicationId: application.applicationId
});
} catch (error) {
console.error('Submit application error:', error);
res.status(500).json({ success: false, message: 'Error submitting application' });
}
};
exports.getApplications = async (req, res) => {
try {
const applications = await Application.findAll({
order: [['createdAt', 'DESC']]
});
res.json({ success: true, applications });
} catch (error) {
console.error('Get applications error:', error);
res.status(500).json({ success: false, message: 'Error fetching applications' });
}
};
exports.getApplicationById = async (req, res) => {
try {
const { id } = req.params;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
res.json({ success: true, application });
} catch (error) {
console.error('Get application error:', error);
res.status(500).json({ success: false, message: 'Error fetching application' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments, rating } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
overallStatus: action,
updatedAt: new Date()
});
res.json({ success: true, message: 'Action taken successfully' });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -1,269 +0,0 @@
const bcrypt = require('bcryptjs');
const { User, AuditLog } = require('../models');
const { generateToken } = require('../config/auth');
const { AUDIT_ACTIONS } = require('../config/constants');
// Register new user
exports.register = async (req, res) => {
try {
const { email, password, fullName, role, phone, region, zone } = req.body;
// Validate input
if (!email || !password || !fullName || !role) {
return res.status(400).json({
success: false,
message: 'Email, password, full name, 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 password
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user
const user = await User.create({
email,
password: hashedPassword,
name: fullName,
role,
phone,
region,
zone
});
// Log audit
await AuditLog.create({
userId: user.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'user',
entityId: user.id
});
res.status(201).json({
success: true,
message: 'User registered successfully',
userId: user.id
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
message: 'Error registering user'
});
}
};
// Login
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Email and password are required'
});
}
// Get user
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Check if account is active
if (user.status !== 'active') {
return res.status(403).json({
success: false,
message: 'Account is deactivated'
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Update last login
await user.update({ lastLogin: new Date() });
// Generate token
const token = generateToken(user);
// Log audit
await AuditLog.create({
userId: user.id,
action: 'user_login',
entityType: 'user',
entityId: user.id
});
res.json({
success: true,
token,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Error during login'
});
}
};
// Get profile
exports.getProfile = async (req, res) => {
try {
const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'name', 'role', 'region', 'zone', 'phone', 'createdAt']
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone,
phone: user.phone,
createdAt: user.createdAt
}
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Error fetching profile'
});
}
};
// Update profile
exports.updateProfile = async (req, res) => {
try {
const { fullName, phone } = req.body;
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
await user.update({
name: fullName || user.name,
phone: phone || user.phone
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Profile updated successfully'
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({
success: false,
message: 'Error updating profile'
});
}
};
// Change password
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
message: 'Current password and new password are required'
});
}
// Get current user
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Verify current password
const isValid = await bcrypt.compare(currentPassword, user.password);
if (!isValid) {
return res.status(401).json({
success: false,
message: 'Current password is incorrect'
});
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update password
await user.update({ password: hashedPassword });
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'password_changed',
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Error changing password'
});
}
};

View File

@ -1,183 +0,0 @@
const { ConstitutionalChange, Outlet, User, Worknote } = require('../models');
const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize'); // Required for Op.or
exports.submitRequest = async (req, res) => {
try {
const {
outletId, changeType, currentConstitution, proposedConstitution,
reason, effectiveDate, newEntityDetails
} = req.body;
const requestId = `CC-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await ConstitutionalChange.create({
requestId,
outletId,
dealerId: req.user.id,
changeType,
description: reason,
currentConstitution,
proposedConstitution,
effectiveDate,
newEntityDetails: JSON.stringify(newEntityDetails), // Store as JSON string
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Constitutional change request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit constitutional change error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await ConstitutionalChange.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['full_name'] // Changed from 'name' to 'full_name' based on original code
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get constitutional changes error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['full_name', 'email'] // Changed from 'name' to 'full_name'
},
{
model: Worknote,
as: 'worknotes' // Assuming Worknote model is associated as 'worknotes'
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
res.json({ success: true, request });
} catch (error) {
console.error('Get constitutional change details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
const timeline = [...request.timeline, {
stage: 'Review',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action,
remarks: comments
}];
await request.update({
status: action, // Assuming action directly maps to status (e.g., 'Approved', 'Rejected')
timeline,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents, // Assuming documents is an array or object that can be stored directly
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -1,99 +0,0 @@
const { FinancePayment, FnF, Application, Resignation, User, Outlet } = require('../models');
exports.getOnboardingPayments = async (req, res) => {
try {
const payments = await FinancePayment.findAll({
include: [{
model: Application,
as: 'application',
attributes: ['applicantName', 'applicationId']
}],
order: [['createdAt', 'ASC']]
});
res.json({ success: true, payments });
} catch (error) {
console.error('Get onboarding payments error:', error);
res.status(500).json({ success: false, message: 'Error fetching payments' });
}
};
exports.getFnFSettlements = async (req, res) => {
try {
const settlements = await FnF.findAll({
include: [
{
model: Resignation,
as: 'resignation',
attributes: ['resignationId']
},
{
model: Outlet, // Need to ensure Outlet is imported or associated
as: 'outlet',
include: [{
model: User,
as: 'dealer',
attributes: ['name']
}]
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, settlements });
} catch (error) {
console.error('Get F&F settlements error:', error);
res.status(500).json({ success: false, message: 'Error fetching settlements' });
}
};
exports.updatePayment = async (req, res) => {
try {
const { id } = req.params;
const { paidDate, amount, paymentMode, transactionReference, status } = req.body;
const payment = await FinancePayment.findByPk(id);
if (!payment) {
return res.status(404).json({ success: false, message: 'Payment not found' });
}
await payment.update({
paymentDate: paidDate || payment.paymentDate,
amount: amount || payment.amount,
transactionId: transactionReference || payment.transactionId,
paymentStatus: status || payment.paymentStatus,
updatedAt: new Date()
});
res.json({ success: true, message: 'Payment updated successfully' });
} catch (error) {
console.error('Update payment error:', error);
res.status(500).json({ success: false, message: 'Error updating payment' });
}
};
exports.updateFnF = async (req, res) => {
try {
const { id } = req.params;
const {
inventoryClearance, sparesClearance, accountsClearance, legalClearance,
finalSettlementAmount, status
} = req.body;
const fnf = await FnF.findByPk(id);
if (!fnf) {
return res.status(404).json({ success: false, message: 'F&F settlement not found' });
}
await fnf.update({
status: status || fnf.status,
netAmount: finalSettlementAmount || fnf.netAmount,
updatedAt: new Date()
});
res.json({ success: true, message: 'F&F settlement updated successfully' });
} catch (error) {
console.error('Update F&F error:', error);
res.status(500).json({ success: false, message: 'Error updating F&F settlement' });
}
};

View File

@ -1,121 +0,0 @@
const { Region, Zone } = require('../models');
exports.getRegions = async (req, res) => {
try {
const regions = await Region.findAll({
order: [['name', 'ASC']]
});
res.json({ success: true, regions });
} catch (error) {
console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' });
}
};
exports.createRegion = async (req, res) => {
try {
const { regionName } = req.body;
if (!regionName) {
return res.status(400).json({ success: false, message: 'Region name is required' });
}
await Region.create({ name: regionName });
res.status(201).json({ success: true, message: 'Region created successfully' });
} catch (error) {
console.error('Create region error:', error);
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
exports.updateRegion = async (req, res) => {
try {
const { id } = req.params;
const { regionName, isActive } = req.body;
const region = await Region.findByPk(id);
if (!region) {
return res.status(404).json({ success: false, message: 'Region not found' });
}
await region.update({
name: regionName || region.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Region updated successfully' });
} catch (error) {
console.error('Update region error:', error);
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
exports.getZones = async (req, res) => {
try {
const { regionId } = req.query;
const where = {};
if (regionId) {
where.regionId = regionId;
}
const zones = await Zone.findAll({
where,
include: [{
model: Region,
as: 'region',
attributes: ['name']
}],
order: [['name', 'ASC']]
});
res.json({ success: true, zones });
} catch (error) {
console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' });
}
};
exports.createZone = async (req, res) => {
try {
const { regionId, zoneName, zoneCode } = req.body;
if (!regionId || !zoneName) {
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' });
}
await Zone.create({
regionId,
name: zoneName
});
res.status(201).json({ success: true, message: 'Zone created successfully' });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
exports.updateZone = async (req, res) => {
try {
const { id } = req.params;
const { zoneName, zoneCode, isActive } = req.body;
const zone = await Zone.findByPk(id);
if (!zone) {
return res.status(404).json({ success: false, message: 'Zone not found' });
}
await zone.update({
name: zoneName || zone.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Zone updated successfully' });
} catch (error) {
console.error('Update zone error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' });
}
};

View File

@ -1,184 +0,0 @@
const { Outlet, User, Resignation } = require('../models');
// Get all outlets for logged-in dealer
exports.getOutlets = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const outlets = await Outlet.findAll({
where,
include: [
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
},
{
model: Resignation,
as: 'resignations',
required: false,
where: {
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
}
],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
outlets
});
} catch (error) {
console.error('Get outlets error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlets'
});
}
};
// Get specific outlet details
exports.getOutletById = async (req, res) => {
try {
const { id } = req.params;
const outlet = await Outlet.findByPk(id, {
include: [{
model: User,
as: 'dealer',
attributes: ['name', 'email', 'phone']
}]
});
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
// Check if dealer can access this outlet
if (req.user.role === 'Dealer' && outlet.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
outlet
});
} catch (error) {
console.error('Get outlet error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlet'
});
}
};
// Create new outlet (admin only)
exports.createOutlet = async (req, res) => {
try {
const {
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
} = req.body;
// Validate required fields
if (!dealerId || !code || !name || !type || !address || !city || !state) {
return res.status(400).json({
success: false,
message: 'Missing required fields'
});
}
// Check if code already exists
const existing = await Outlet.findOne({ where: { code } });
if (existing) {
return res.status(400).json({
success: false,
message: 'Outlet code already exists'
});
}
const outlet = await Outlet.create({
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
});
res.status(201).json({
success: true,
message: 'Outlet created successfully',
outletId: outlet.id
});
} catch (error) {
console.error('Create outlet error:', error);
res.status(500).json({
success: false,
message: 'Error creating outlet'
});
}
};
// Update outlet
exports.updateOutlet = async (req, res) => {
try {
const { id } = req.params;
const { name, address, city, state, pincode, status, latitude, longitude } = req.body;
const outlet = await Outlet.findByPk(id);
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
await outlet.update({
name: name || outlet.name,
address: address || outlet.address,
city: city || outlet.city,
state: state || outlet.state,
pincode: pincode || outlet.pincode,
status: status || outlet.status,
latitude: latitude || outlet.latitude,
longitude: longitude || outlet.longitude,
updatedAt: new Date()
});
res.json({
success: true,
message: 'Outlet updated successfully'
});
} catch (error) {
console.error('Update outlet error:', error);
res.status(500).json({
success: false,
message: 'Error updating outlet'
});
}
};

View File

@ -1,238 +0,0 @@
const { RelocationRequest, Outlet, User, Worknote } = require('../models');
const { Op } = require('sequelize');
const { v4: uuidv4 } = require('uuid');
exports.submitRequest = async (req, res) => {
try {
const {
outletId, relocationType, currentAddress, currentCity, currentState,
currentLatitude, currentLongitude, proposedAddress, proposedCity,
proposedState, proposedLatitude, proposedLongitude, reason, proposedDate
} = req.body;
const requestId = `REL-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await RelocationRequest.create({
requestId,
outletId,
dealerId: req.user.id,
relocationType,
currentAddress,
currentCity,
currentState,
currentLatitude,
currentLongitude,
proposedAddress,
proposedCity,
proposedState,
proposedLatitude,
proposedLongitude,
reason,
proposedDate,
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Relocation request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit relocation error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await RelocationRequest.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['full_name'] // Changed 'name' to 'full_name' based on original query
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get relocation requests error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['full_name', 'email'] // Changed 'name' to 'full_name'
},
{
model: Worknote,
as: 'worknotes', // Assuming Worknote model is for workflow/comments
include: [{
model: User,
as: 'actionedBy', // Assuming Worknote has an association to User for actioned_by
attributes: ['full_name']
}],
order: [['createdAt', 'ASC']]
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Calculate distance between current and proposed location (retained from original)
if (request.currentLatitude && request.proposedLatitude) {
const distance = calculateDistance(
request.currentLatitude, request.currentLongitude,
request.proposedLatitude, request.proposedLongitude
);
request.dataValues.distance = `${distance.toFixed(2)} km`; // Add to dataValues for response
}
res.json({ success: true, request });
} catch (error) {
console.error('Get relocation details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Update status and current_stage based on action
let newStatus = request.status;
let newCurrentStage = request.currentStage;
if (action === 'Approved') {
newStatus = 'Approved';
// Assuming next stage logic would be here, e.g., 'Final Approval'
} else if (action === 'Rejected') {
newStatus = 'Rejected';
} else if (action === 'Forwarded to RBM') {
newCurrentStage = 'RBM Review';
} else if (action === 'Forwarded to ZBM') {
newCurrentStage = 'ZBM Review';
} else if (action === 'Forwarded to HO') {
newCurrentStage = 'HO Review';
}
// Create a worknote entry
await Worknote.create({
requestId: request.id,
stage: newCurrentStage, // Or the specific stage where action was taken
action: action,
comments: comments,
actionedBy: req.user.id,
actionedAt: new Date()
});
// Update the request status and current stage
await request.update({
status: newStatus,
currentStage: newCurrentStage,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};
// Helper function to calculate distance between two coordinates
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radius of Earth in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}

View File

@ -1,417 +0,0 @@
const db = require('../models');
const logger = require('../utils/logger');
const { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } = require('../config/constants');
const { Op } = require('sequelize');
// Generate unique resignation ID
const generateResignationId = async () => {
const count = await db.Resignation.count();
return `RES-${String(count + 1).padStart(3, '0')}`;
};
// Calculate progress percentage based on stage
const calculateProgress = (stage) => {
const stageProgress = {
[RESIGNATION_STAGES.ASM]: 15,
[RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 70,
[RESIGNATION_STAGES.LEGAL]: 80,
[RESIGNATION_STAGES.FINANCE]: 90,
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
[RESIGNATION_STAGES.COMPLETED]: 100,
[RESIGNATION_STAGES.REJECTED]: 0
};
return stageProgress[stage] || 0;
};
// Create resignation request (Dealer only)
exports.createResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { outletId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo } = req.body;
const dealerId = req.user.id;
// Verify outlet belongs to dealer
const outlet = await db.Outlet.findOne({
where: { id: outletId, dealerId }
});
if (!outlet) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Outlet not found or does not belong to you'
});
}
// Check if outlet already has active resignation
const existingResignation = await db.Resignation.findOne({
where: {
outletId,
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
});
if (existingResignation) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'This outlet already has an active resignation request'
});
}
// Generate resignation ID
const resignationId = await generateResignationId();
// Create resignation
const resignation = await db.Resignation.create({
resignationId,
outletId,
dealerId,
resignationType,
lastOperationalDateSales,
lastOperationalDateServices,
reason,
additionalInfo,
currentStage: RESIGNATION_STAGES.ASM,
status: 'ASM Review',
progressPercentage: 15,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
action: 'Resignation request submitted'
}]
}, { transaction });
// Update outlet status
await outlet.update({
status: 'Pending Resignation'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.CREATED,
entityType: 'resignation',
entityId: resignation.id,
changes: { created: resignation.toJSON() }
}, { transaction });
await transaction.commit();
logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`);
// TODO: Send email notification to ASM/DD Admin
res.status(201).json({
success: true,
message: 'Resignation request submitted successfully',
resignationId: resignation.resignationId,
resignation: resignation.toJSON()
});
} catch (error) {
await transaction.rollback();
logger.error('Error creating resignation:', error);
next(error);
}
};
// Get resignations list (role-based filtering)
exports.getResignations = async (req, res, next) => {
try {
const { status, region, zone, page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
// Build where clause based on user role
let where = {};
if (req.user.role === ROLES.DEALER) {
// Dealers see only their resignations
where.dealerId = req.user.id;
} else if (req.user.region && ![ROLES.NBH, ROLES.DD_HEAD, ROLES.DD_LEAD, ROLES.SUPER_ADMIN].includes(req.user.role)) {
// Regional users see resignations in their region
where['$outlet.region$'] = req.user.region;
}
if (status) {
where.status = status;
}
// Get resignations
const { count, rows: resignations } = await db.Resignation.findAndCountAll({
where,
include: [
{
model: db.Outlet,
as: 'outlet',
attributes: ['id', 'code', 'name', 'type', 'city', 'state', 'region', 'zone']
},
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
],
order: [['submittedOn', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
resignations,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
});
} catch (error) {
logger.error('Error fetching resignations:', error);
next(error);
}
};
// Get resignation details
exports.getResignationById = async (req, res, next) => {
try {
const { id } = req.params;
const resignation = await db.Resignation.findOne({
where: { id },
include: [
{
model: db.Outlet,
as: 'outlet',
include: [
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
]
},
{
model: db.Worknote,
as: 'worknotes',
order: [['timestamp', 'DESC']]
}
]
});
if (!resignation) {
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Check access permissions
if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
resignation
});
} catch (error) {
logger.error('Error fetching resignation details:', error);
next(error);
}
};
// Approve resignation (move to next stage)
exports.approveResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { remarks } = req.body;
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Determine next stage based on current stage
const stageFlow = {
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE,
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
};
const nextStage = stageFlow[resignation.currentStage];
if (!nextStage) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Cannot approve from current stage'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: nextStage,
timestamp: new Date(),
user: req.user.name,
action: 'Approved',
remarks
}];
await resignation.update({
currentStage: nextStage,
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`,
progressPercentage: calculateProgress(nextStage),
timeline
}, { transaction });
// If completed, update outlet status
if (nextStage === RESIGNATION_STAGES.COMPLETED) {
await resignation.outlet.update({
status: 'Closed'
}, { transaction });
}
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.APPROVED,
entityType: 'resignation',
entityId: resignation.id,
changes: {
from: resignation.currentStage,
to: nextStage,
remarks
}
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`);
// TODO: Send email notification to next approver
res.json({
success: true,
message: 'Resignation approved successfully',
nextStage,
resignation
});
} catch (error) {
await transaction.rollback();
logger.error('Error approving resignation:', error);
next(error);
}
};
// Reject resignation
exports.rejectResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { reason } = req.body;
if (!reason) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Rejection reason is required'
});
}
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: 'Rejected',
timestamp: new Date(),
user: req.user.name,
action: 'Rejected',
reason
}];
await resignation.update({
currentStage: RESIGNATION_STAGES.REJECTED,
status: 'Rejected',
progressPercentage: 0,
rejectionReason: reason,
timeline
}, { transaction });
// Update outlet status back to Active
await resignation.outlet.update({
status: 'Active'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.REJECTED,
entityType: 'resignation',
entityId: resignation.id,
changes: { reason }
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`);
// TODO: Send email notification to dealer
res.json({
success: true,
message: 'Resignation rejected',
resignation
});
} catch (error) {
await transaction.rollback();
logger.error('Error rejecting resignation:', error);
next(error);
}
};
module.exports = exports;

View File

@ -1,84 +0,0 @@
const { Worknote, User } = require('../models');
exports.addWorknote = async (req, res) => {
try {
const { requestId, requestType, message, isInternal } = req.body;
if (!requestId || !requestType || !message) {
return res.status(400).json({
success: false,
message: 'Request ID, type, and message are required'
});
}
await Worknote.create({
requestId,
requestType,
userId: req.user.id,
content: message,
isInternal: isInternal || false
});
res.status(201).json({
success: true,
message: 'Worknote added successfully'
});
} catch (error) {
console.error('Add worknote error:', error);
res.status(500).json({ success: false, message: 'Error adding worknote' });
}
};
exports.getWorknotes = async (req, res) => {
try {
const { requestId } = req.params;
const { requestType } = req.query;
const worknotes = await Worknote.findAll({
where: {
requestId,
requestType
},
include: [{
model: User,
as: 'author',
attributes: ['name', 'role']
}],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
worknotes
});
} catch (error) {
console.error('Get worknotes error:', error);
res.status(500).json({ success: false, message: 'Error fetching worknotes' });
}
};
exports.deleteWorknote = async (req, res) => {
try {
const { id } = req.params;
const worknote = await Worknote.findByPk(id);
if (!worknote) {
return res.status(404).json({ success: false, message: 'Worknote not found' });
}
// Only allow user who created it or admin to delete
if (worknote.userId !== req.user.id && req.user.role !== 'Super Admin') {
return res.status(403).json({ success: false, message: 'Access denied' });
}
await worknote.destroy();
res.json({
success: true,
message: 'Worknote deleted successfully'
});
} catch (error) {
console.error('Delete worknote error:', error);
res.status(500).json({ success: false, message: 'Error deleting worknote' });
}
};

View File

@ -17,6 +17,7 @@ erDiagram
string department string department
string designation string designation
uuid role_code FK uuid role_code FK
string role_code UK
uuid zone_id FK uuid zone_id FK
uuid region_id FK uuid region_id FK
uuid state_id FK uuid state_id FK
@ -48,7 +49,7 @@ erDiagram
string permission_code UK string permission_code UK
string permission_name string permission_name
string module string module
string permission_type string permission_category
string action string action
string description string description
timestamp created_at timestamp created_at
@ -58,11 +59,6 @@ erDiagram
uuid role_permission_id PK uuid role_permission_id PK
uuid role_id FK uuid role_id FK
uuid permission_id FK uuid permission_id FK
boolean can_view
boolean can_create
boolean can_edit
boolean can_delete
boolean can_approve
timestamp created_at timestamp created_at
} }
@ -115,6 +111,8 @@ erDiagram
DISTRICTS { DISTRICTS {
uuid district_id PK uuid district_id PK
uuid state_id FK uuid state_id FK
uuid zone_id FK
uuid region_id FK
string district_name string district_name
boolean is_active boolean is_active
timestamp created_at timestamp created_at
@ -122,7 +120,9 @@ erDiagram
AREAS { AREAS {
uuid area_id PK uuid area_id PK
uuid zone_id FK
uuid region_id FK uuid region_id FK
uuid state_id FK
uuid district_id FK uuid district_id FK
string area_code UK string area_code UK
string area_name string area_name
@ -1067,15 +1067,19 @@ erDiagram
%% RELATIONSHIPS %% RELATIONSHIPS
%% ============================================ %% ============================================
USERS ||--o{ USER_ROLES : "has" USERS ||--o{ USER_ROLES : "has"
ROLES ||--o{ USER_ROLES : "assigned_to" ROLES ||--o{ USERS : "assigned_to"
ROLES ||--o{ ROLE_PERMISSIONS : "has" ROLES ||--o{ ROLE_PERMISSIONS : "has"
PERMISSIONS ||--o{ ROLE_PERMISSIONS : "granted_in" PERMISSIONS ||--o{ ROLE_PERMISSIONS : "granted_in"
ZONES ||--o{ STATES : "contains" ZONES ||--o{ STATES : "contains"
STATES ||--o{ DISTRICTS : "contains" STATES ||--o{ DISTRICTS : "contains"
ZONES ||--o{ DISTRICTS : "contains"
REGIONS ||--o{ DISTRICTS : "contains"
ZONES ||--o{ REGIONS : "contains" ZONES ||--o{ REGIONS : "contains"
STATES ||--o{ REGIONS : "contains" STATES ||--o{ REGIONS : "contains"
ZONES ||--o{ AREAS : "contains"
REGIONS ||--o{ AREAS : "contains" REGIONS ||--o{ AREAS : "contains"
STATES ||--o{ AREAS : "contains"
DISTRICTS ||--o{ AREAS : "contains" DISTRICTS ||--o{ AREAS : "contains"
ZONES ||--o{ ZONE_MANAGERS : "managed_by" ZONES ||--o{ ZONE_MANAGERS : "managed_by"

View File

@ -1,100 +0,0 @@
const jwt = require('jsonwebtoken');
const db = require('../models');
const logger = require('../utils/logger');
const authenticate = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const token = authHeader.replace('Bearer ', '');
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Find user
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token. User not found.'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: 'User account is inactive.'
});
}
// Attach user to request
req.user = user;
req.token = token;
next();
} catch (error) {
logger.error('Authentication error:', error);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
res.status(500).json({
success: false,
message: 'Authentication failed'
});
}
};
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}
const token = authHeader.replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (user && user.status === 'active') {
req.user = user;
req.token = token;
}
next();
} catch (error) {
// If token is invalid/expired, just proceed without user
next();
}
};
module.exports = {
authenticate,
optionalAuth
};

View File

@ -1,76 +0,0 @@
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Error occurred:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip
});
// Sequelize validation errors
if (err.name === 'SequelizeValidationError') {
const errors = err.errors.map(e => ({
field: e.path,
message: e.message
}));
return res.status(400).json({
success: false,
message: 'Validation error',
errors
});
}
// Sequelize unique constraint errors
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
success: false,
message: 'Resource already exists',
field: err.errors[0]?.path
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
// Multer file upload errors
if (err.name === 'MulterError') {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
message: 'File too large'
});
}
return res.status(400).json({
success: false,
message: err.message
});
}
// Default error
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal server error';
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;

View File

@ -1,47 +0,0 @@
const { ROLES } = require('../config/constants');
const logger = require('../utils/logger');
/**
* Role-based access control middleware
* @param {Array<string>} allowedRoles - Array of roles that can access the route
* @returns {Function} Express middleware function
*/
const checkRole = (allowedRoles) => {
return (req, res, next) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
// Check if user role is in allowed roles
if (!allowedRoles.includes(req.user.role)) {
logger.warn(`Access denied for user ${req.user.email} (${req.user.role}) to route ${req.path}`);
return res.status(403).json({
success: false,
message: 'Access denied. Insufficient permissions.',
requiredRoles: allowedRoles,
yourRole: req.user.role
});
}
// User has required role, proceed
next();
} catch (error) {
logger.error('Role check error:', error);
res.status(500).json({
success: false,
message: 'Authorization check failed'
});
}
};
};
module.exports = {
checkRole,
ROLES
};

View File

@ -1,101 +0,0 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
// Create uploads directory if it doesn't exist
const uploadDir = process.env.UPLOAD_DIR || './uploads';
const documentsDir = path.join(uploadDir, 'documents');
const profilesDir = path.join(uploadDir, 'profiles');
const tempDir = path.join(uploadDir, 'temp');
[uploadDir, documentsDir, profilesDir, tempDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// Storage configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
let folder = documentsDir;
if (req.body.uploadType === 'profile') {
folder = profilesDir;
} else if (req.body.uploadType === 'temp') {
folder = tempDir;
}
cb(null, folder);
},
filename: (req, file, cb) => {
const uniqueId = uuidv4();
const ext = path.extname(file.originalname);
const filename = `${uniqueId}${ext}`;
cb(null, filename);
}
});
// File filter
const fileFilter = (req, file, cb) => {
// Allowed file types
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only PDF, JPG, PNG, DOC, DOCX, XLS, XLSX allowed'), false);
}
};
// Multer upload configuration
const upload = multer({
storage: storage,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024 // 10MB default
},
fileFilter: fileFilter
});
// Single file upload
const uploadSingle = upload.single('file');
// Multiple files upload
const uploadMultiple = upload.array('files', 10); // Max 10 files
// Error handler for multer
const handleUploadError = (err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: 'File too large. Maximum size is 10MB'
});
}
return res.status(400).json({
success: false,
message: `Upload error: ${err.message}`
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
next();
};
module.exports = {
uploadSingle,
uploadMultiple,
handleUploadError
};

View File

@ -1,104 +0,0 @@
const { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Application = sequelize.define('Application', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
applicantName: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: { isEmail: true }
},
phone: {
type: DataTypes.STRING,
allowNull: false
},
businessType: {
type: DataTypes.ENUM(Object.values(BUSINESS_TYPES)),
allowNull: false
},
preferredLocation: {
type: DataTypes.STRING,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
experienceYears: {
type: DataTypes.INTEGER,
allowNull: false
},
investmentCapacity: {
type: DataTypes.STRING,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(APPLICATION_STAGES)),
defaultValue: APPLICATION_STAGES.DD
},
overallStatus: {
type: DataTypes.ENUM(Object.values(APPLICATION_STATUS)),
defaultValue: APPLICATION_STATUS.PENDING
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
submittedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'applications',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['email'] },
{ fields: ['currentStage'] },
{ fields: ['overallStatus'] }
]
});
Application.associate = (models) => {
Application.belongsTo(models.User, {
foreignKey: 'submittedBy',
as: 'submitter'
});
Application.hasMany(models.Document, {
foreignKey: 'requestId',
as: 'uploadedDocuments',
scope: { requestType: 'application' }
});
};
return Application;
};

View File

@ -1,65 +0,0 @@
const { AUDIT_ACTIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const AuditLog = sequelize.define('AuditLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
action: {
type: DataTypes.ENUM(Object.values(AUDIT_ACTIONS)),
allowNull: false
},
entityType: {
type: DataTypes.STRING,
allowNull: false
},
entityId: {
type: DataTypes.UUID,
allowNull: false
},
oldData: {
type: DataTypes.JSON,
allowNull: true
},
newData: {
type: DataTypes.JSON,
allowNull: true
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
userAgent: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'audit_logs',
timestamps: true,
indexes: [
{ fields: ['userId'] },
{ fields: ['action'] },
{ fields: ['entityType'] },
{ fields: ['entityId'] }
]
});
AuditLog.associate = (models) => {
AuditLog.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return AuditLog;
};

View File

@ -1,87 +0,0 @@
const { CONSTITUTIONAL_CHANGE_TYPES, CONSTITUTIONAL_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const ConstitutionalChange = sequelize.define('ConstitutionalChange', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
changeType: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_CHANGE_TYPES)),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_STAGES)),
defaultValue: CONSTITUTIONAL_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'constitutional_changes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
ConstitutionalChange.associate = (models) => {
ConstitutionalChange.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
ConstitutionalChange.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
ConstitutionalChange.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'constitutional' }
});
};
return ConstitutionalChange;
};

View File

@ -1,64 +0,0 @@
const { REQUEST_TYPES, DOCUMENT_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Document = sequelize.define('Document', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
documentType: {
type: DataTypes.ENUM(Object.values(DOCUMENT_TYPES)),
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
fileUrl: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
status: {
type: DataTypes.STRING,
defaultValue: 'Active'
}
}, {
tableName: 'documents',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['documentType'] }
]
});
Document.associate = (models) => {
Document.belongsTo(models.User, {
foreignKey: 'uploadedBy',
as: 'uploader'
});
};
return Document;
};

View File

@ -1,75 +0,0 @@
const { PAYMENT_TYPES, PAYMENT_STATUS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const FinancePayment = sequelize.define('FinancePayment', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'applications',
key: 'id'
}
},
paymentType: {
type: DataTypes.ENUM(Object.values(PAYMENT_TYPES)),
allowNull: false
},
amount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
paymentStatus: {
type: DataTypes.ENUM(Object.values(PAYMENT_STATUS)),
defaultValue: PAYMENT_STATUS.PENDING
},
transactionId: {
type: DataTypes.STRING,
allowNull: true
},
paymentDate: {
type: DataTypes.DATE,
allowNull: true
},
verifiedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
verificationDate: {
type: DataTypes.DATE,
allowNull: true
},
remarks: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'finance_payments',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['paymentStatus'] }
]
});
FinancePayment.associate = (models) => {
FinancePayment.belongsTo(models.Application, {
foreignKey: 'applicationId',
as: 'application'
});
FinancePayment.belongsTo(models.User, {
foreignKey: 'verifiedBy',
as: 'verifier'
});
};
return FinancePayment;
};

View File

@ -1,72 +0,0 @@
const { FNF_STATUS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const FnF = sequelize.define('FnF', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resignations',
key: 'id'
}
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
status: {
type: DataTypes.ENUM(Object.values(FNF_STATUS)),
defaultValue: FNF_STATUS.INITIATED
},
totalReceivables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
totalPayables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
netAmount: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
settlementDate: {
type: DataTypes.DATE,
allowNull: true
},
clearanceDocuments: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'fnf_settlements',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['status'] }
]
});
FnF.associate = (models) => {
FnF.belongsTo(models.Resignation, {
foreignKey: 'resignationId',
as: 'resignation'
});
FnF.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
};
return FnF;
};

View File

@ -1,104 +0,0 @@
const { OUTLET_TYPES, OUTLET_STATUS, REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Outlet = sequelize.define('Outlet', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
code: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.ENUM(Object.values(OUTLET_TYPES)),
allowNull: false
},
address: {
type: DataTypes.TEXT,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
pincode: {
type: DataTypes.STRING,
allowNull: false
},
latitude: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true
},
longitude: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true
},
status: {
type: DataTypes.ENUM(Object.values(OUTLET_STATUS)),
defaultValue: OUTLET_STATUS.ACTIVE
},
establishedDate: {
type: DataTypes.DATEONLY,
allowNull: false
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: false
},
zone: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'outlets',
timestamps: true,
indexes: [
{ fields: ['code'] },
{ fields: ['dealerId'] },
{ fields: ['type'] },
{ fields: ['status'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
Outlet.associate = (models) => {
Outlet.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Outlet.hasMany(models.Resignation, {
foreignKey: 'outletId',
as: 'resignations'
});
Outlet.hasMany(models.ConstitutionalChange, {
foreignKey: 'outletId',
as: 'constitutionalChanges'
});
Outlet.hasMany(models.RelocationRequest, {
foreignKey: 'outletId',
as: 'relocationRequests'
});
};
return Outlet;
};

View File

@ -1,44 +0,0 @@
const { REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Region = sequelize.define('Region', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.ENUM(Object.values(REGIONS)),
unique: true,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
regionalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'regions',
timestamps: true
});
Region.associate = (models) => {
Region.belongsTo(models.User, {
foreignKey: 'regionalManagerId',
as: 'regionalManager'
});
Region.hasMany(models.Zone, {
foreignKey: 'regionId',
as: 'zones'
});
};
return Region;
};

View File

@ -1,99 +0,0 @@
const { RELOCATION_TYPES, RELOCATION_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const RelocationRequest = sequelize.define('RelocationRequest', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
relocationType: {
type: DataTypes.ENUM(Object.values(RELOCATION_TYPES)),
allowNull: false
},
newAddress: {
type: DataTypes.TEXT,
allowNull: false
},
newCity: {
type: DataTypes.STRING,
allowNull: false
},
newState: {
type: DataTypes.STRING,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(RELOCATION_STAGES)),
defaultValue: RELOCATION_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'relocation_requests',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
RelocationRequest.associate = (models) => {
RelocationRequest.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
RelocationRequest.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
RelocationRequest.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'relocation' }
});
};
return RelocationRequest;
};

View File

@ -1,110 +0,0 @@
const { RESIGNATION_TYPES, RESIGNATION_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Resignation = sequelize.define('Resignation', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resignationType: {
type: DataTypes.ENUM(Object.values(RESIGNATION_TYPES)),
allowNull: false
},
lastOperationalDateSales: {
type: DataTypes.DATEONLY,
allowNull: false
},
lastOperationalDateServices: {
type: DataTypes.DATEONLY,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
additionalInfo: {
type: DataTypes.TEXT,
allowNull: true
},
currentStage: {
type: DataTypes.ENUM(Object.values(RESIGNATION_STAGES)),
defaultValue: RESIGNATION_STAGES.ASM
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
submittedOn: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
},
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'resignations',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] },
{ fields: ['status'] }
]
});
Resignation.associate = (models) => {
Resignation.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
Resignation.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Resignation.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: {
requestType: 'resignation'
}
});
};
return Resignation;
};

View File

@ -1,89 +0,0 @@
const { ROLES, REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.ENUM(Object.values(ROLES)),
allowNull: false
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: true
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
},
lastLogin: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['email'] },
{ fields: ['role'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
User.associate = (models) => {
User.hasMany(models.Application, {
foreignKey: 'submittedBy',
as: 'applications'
});
User.hasMany(models.Outlet, {
foreignKey: 'dealerId',
as: 'outlets'
});
User.hasMany(models.Resignation, {
foreignKey: 'dealerId',
as: 'resignations'
});
User.hasMany(models.ConstitutionalChange, {
foreignKey: 'dealerId',
as: 'constitutionalChanges'
});
User.hasMany(models.RelocationRequest, {
foreignKey: 'dealerId',
as: 'relocationRequests'
});
User.hasMany(models.AuditLog, {
foreignKey: 'userId',
as: 'auditLogs'
});
};
return User;
};

View File

@ -1,52 +0,0 @@
const { REQUEST_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Worknote = sequelize.define('Worknote', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
isInternal: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'worknotes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['userId'] }
]
});
Worknote.associate = (models) => {
Worknote.belongsTo(models.User, {
foreignKey: 'userId',
as: 'author'
});
};
return Worknote;
};

View File

@ -1,49 +0,0 @@
module.exports = (sequelize, DataTypes) => {
const Zone = sequelize.define('Zone', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
zonalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'zones',
timestamps: true,
indexes: [
{ fields: ['regionId'] },
{ unique: true, fields: ['name', 'regionId'] }
]
});
Zone.associate = (models) => {
Zone.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
Zone.belongsTo(models.User, {
foreignKey: 'zonalManagerId',
as: 'zonalManager'
});
};
return Zone;
};

View File

@ -1,49 +0,0 @@
const { Sequelize } = require('sequelize');
const config = require('../config/database');
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
// Initialize Sequelize
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: dbConfig.logging,
pool: dbConfig.pool,
dialectOptions: dbConfig.dialectOptions
}
);
const db = {};
// Import models
db.User = require('./User')(sequelize, Sequelize.DataTypes);
db.Application = require('./Application')(sequelize, Sequelize.DataTypes);
db.Resignation = require('./Resignation')(sequelize, Sequelize.DataTypes);
db.ConstitutionalChange = require('./ConstitutionalChange')(sequelize, Sequelize.DataTypes);
db.RelocationRequest = require('./RelocationRequest')(sequelize, Sequelize.DataTypes);
db.Outlet = require('./Outlet')(sequelize, Sequelize.DataTypes);
db.Worknote = require('./Worknote')(sequelize, Sequelize.DataTypes);
db.Document = require('./Document')(sequelize, Sequelize.DataTypes);
db.AuditLog = require('./AuditLog')(sequelize, Sequelize.DataTypes);
db.FinancePayment = require('./FinancePayment')(sequelize, Sequelize.DataTypes);
db.FnF = require('./FnF')(sequelize, Sequelize.DataTypes);
db.Region = require('./Region')(sequelize, Sequelize.DataTypes);
db.Zone = require('./Zone')(sequelize, Sequelize.DataTypes);
// Define associations
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@ -9,7 +9,8 @@
"dev": "tsx watch src/server.ts", "dev": "tsx watch src/server.ts",
"build": "tsc", "build": "tsc",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"migrate": "node dist/scripts/migrate.js", "migrate": "tsx scripts/migrate.ts",
"seed": "tsx scripts/seed-geo.ts",
"test": "jest", "test": "jest",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
"clear-logs": "rm -rf logs/*.log" "clear-logs": "rm -rf logs/*.log"

View File

@ -1,15 +0,0 @@
const express = require('express');
const router = express.Router();
const applicationController = require('../controllers/applicationController');
const { authenticate, optionalAuth } = require('../middleware/auth');
// Public route - submit application
router.post('/', applicationController.submitApplication);
// Protected routes
router.get('/', authenticate, applicationController.getApplications);
router.get('/:id', authenticate, applicationController.getApplicationById);
router.put('/:id/action', authenticate, applicationController.takeAction);
router.post('/:id/documents', authenticate, applicationController.uploadDocuments);
module.exports = router;

View File

@ -1,15 +0,0 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/auth');
// Public routes
router.post('/register', authController.register);
router.post('/login', authController.login);
// Protected routes
router.get('/profile', authenticate, authController.getProfile);
router.put('/profile', authenticate, authController.updateProfile);
router.post('/change-password', authenticate, authController.changePassword);
module.exports = router;

View File

@ -1,24 +0,0 @@
const express = require('express');
const router = express.Router();
const constitutionalController = require('../controllers/constitutionalController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Submit constitutional change request
router.post('/', constitutionalController.submitRequest);
// Get constitutional change requests
router.get('/', constitutionalController.getRequests);
// Get specific request details
router.get('/:id', constitutionalController.getRequestById);
// Take action on request
router.put('/:id/action', constitutionalController.takeAction);
// Upload documents
router.post('/:id/documents', constitutionalController.uploadDocuments);
module.exports = router;

View File

@ -1,16 +0,0 @@
const express = require('express');
const router = express.Router();
const financeController = require('../controllers/financeController');
const { authenticate } = require('../middleware/auth');
const { checkRole, ROLES } = require('../middleware/roleCheck');
// All routes require authentication
router.use(authenticate);
// Finance user only routes
router.get('/onboarding', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.getOnboardingPayments);
router.get('/fnf', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.getFnFSettlements);
router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.updatePayment);
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.updateFnF);
module.exports = router;

View File

@ -1,20 +0,0 @@
const express = require('express');
const router = express.Router();
const masterController = require('../controllers/masterController');
const { authenticate } = require('../middleware/auth');
const { checkRole, ROLES } = require('../middleware/roleCheck');
// All routes require authentication
router.use(authenticate);
// Regions
router.get('/regions', masterController.getRegions);
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createRegion);
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateRegion);
// Zones
router.get('/zones', masterController.getZones);
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createZone);
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateZone);
module.exports = router;

View File

@ -1,21 +0,0 @@
const express = require('express');
const router = express.Router();
const outletController = require('../controllers/outletController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Get all outlets for logged-in dealer
router.get('/', outletController.getOutlets);
// Get specific outlet details
router.get('/:id', outletController.getOutletById);
// Create new outlet (admin only)
router.post('/', outletController.createOutlet);
// Update outlet
router.put('/:id', outletController.updateOutlet);
module.exports = router;

View File

@ -1,24 +0,0 @@
const express = require('express');
const router = express.Router();
const relocationController = require('../controllers/relocationController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Submit relocation request
router.post('/', relocationController.submitRequest);
// Get relocation requests
router.get('/', relocationController.getRequests);
// Get specific request details
router.get('/:id', relocationController.getRequestById);
// Take action on request
router.put('/:id/action', relocationController.takeAction);
// Upload documents
router.post('/:id/documents', relocationController.uploadDocuments);
module.exports = router;

View File

@ -1,62 +0,0 @@
const express = require('express');
const router = express.Router();
const resignationController = require('../controllers/resignationController');
const { authenticate } = require('../middleware/auth');
const { checkRole } = require('../middleware/roleCheck');
const { ROLES } = require('../config/constants');
// Create resignation (Dealer only)
router.post(
'/create',
authenticate,
checkRole([ROLES.DEALER]),
resignationController.createResignation
);
// Get resignations list (role-based filtering)
router.get(
'/list',
authenticate,
resignationController.getResignations
);
// Get resignation by ID
router.get(
'/:id',
authenticate,
resignationController.getResignationById
);
// Approve resignation (specific roles only)
router.post(
'/:id/approve',
authenticate,
checkRole([
ROLES.RBM,
ROLES.ZBH,
ROLES.NBH,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.FINANCE,
ROLES.SUPER_ADMIN
]),
resignationController.approveResignation
);
// Reject resignation (specific roles only)
router.post(
'/:id/reject',
authenticate,
checkRole([
ROLES.RBM,
ROLES.ZBH,
ROLES.NBH,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.FINANCE,
ROLES.SUPER_ADMIN
]),
resignationController.rejectResignation
);
module.exports = router;

View File

@ -1,65 +0,0 @@
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
const { uploadSingle, uploadMultiple, handleUploadError } = require('../middleware/upload');
// All routes require authentication
router.use(authenticate);
// Single file upload
router.post('/document', (req, res, next) => {
uploadSingle(req, res, (err) => {
if (err) {
return handleUploadError(err, req, res, next);
}
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
res.json({
success: true,
message: 'File uploaded successfully',
file: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
url: `/uploads/${req.body.uploadType || 'documents'}/${req.file.filename}`
}
});
});
});
// Multiple files upload
router.post('/documents', (req, res, next) => {
uploadMultiple(req, res, (err) => {
if (err) {
return handleUploadError(err, req, res, next);
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded'
});
}
const files = req.files.map(file => ({
filename: file.filename,
originalName: file.originalname,
size: file.size,
url: `/uploads/${req.body.uploadType || 'documents'}/${file.filename}`
}));
res.json({
success: true,
message: 'Files uploaded successfully',
files
});
});
});
module.exports = router;

View File

@ -1,18 +0,0 @@
const express = require('express');
const router = express.Router();
const worknoteController = require('../controllers/worknoteController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Add worknote to a request
router.post('/', worknoteController.addWorknote);
// Get worknotes for a request
router.get('/:requestId', worknoteController.getWorknotes);
// Delete worknote (admin only)
router.delete('/:id', worknoteController.deleteWorknote);
module.exports = router;

View File

@ -1,40 +0,0 @@
/**
* Database Migration Script
* Synchronizes all Sequelize models with the database
* This script will DROP all existing tables and recreate them.
*
* Run: node scripts/migrate.js
*/
require('dotenv').config();
const db = require('../src/database/models');
async function runMigrations() {
console.log('🔄 Starting database synchronization (Fresh Startup)...\n');
console.log('⚠️ WARNING: This will drop all existing tables in the database.\n');
try {
// Authenticate with the database
await db.sequelize.authenticate();
console.log('📡 Connected to the database successfully.');
// Synchronize models (force: true drops existing tables)
// This ensures that the schema exactly matches the Sequelize models
await db.sequelize.sync({ force: true });
console.log('\n✅ All tables created and synchronized successfully!');
console.log('----------------------------------------------------');
console.log(`Available Models: ${Object.keys(db).filter(k => k !== 'sequelize' && k !== 'Sequelize').join(', ')}`);
console.log('----------------------------------------------------');
process.exit(0);
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
if (error.stack) {
console.error('\nStack Trace:\n', error.stack);
}
process.exit(1);
}
}
runMigrations();

27
scripts/seed-geo.ts Normal file
View File

@ -0,0 +1,27 @@
import { createRequire } from 'module';
import db from '../src/database/models/index.js';
const require = createRequire(import.meta.url);
const geoSeeder = require('../seeders/20240127-seed-geo-data.js');
async function seed() {
console.log('🌱 Starting Geo Data Seeding...');
try {
await db.sequelize.authenticate();
console.log('Connection established.');
const queryInterface = db.sequelize.getQueryInterface();
console.log('Executing seeder...');
await geoSeeder.up(queryInterface, db.Sequelize);
console.log('✅ Seeding completed successfully.');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seed();

310
scripts/seed-permissions.ts Normal file
View File

@ -0,0 +1,310 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
import { PERMISSIONS, PERMISSION_CATEGORIES } from '../src/common/config/permissions.js';
const { Permission } = db;
const permissionsToSeed = [
// Action Permissions
{
code: PERMISSIONS.ACTION_APPROVE,
name: 'Approve Applications',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'APPROVE',
description: 'Ability to approve applications'
},
{
code: PERMISSIONS.ACTION_REJECT,
name: 'Reject Applications',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'REJECT',
description: 'Ability to reject applications'
},
{
code: PERMISSIONS.ACTION_UPLOAD_DOCS,
name: 'Upload Documents',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'UPLOAD_DOCS',
description: 'Ability to upload documents'
},
{
code: PERMISSIONS.ACTION_REQUEST_CHANGES,
name: 'Request Changes',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'REQUEST_CHANGES',
description: 'Ability to request changes'
},
{
code: PERMISSIONS.ACTION_FORWARD,
name: 'Forward to Others',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'FORWARD',
description: 'Ability to forward applications'
},
{
code: PERMISSIONS.ACTION_REASSIGN,
name: 'Reassign Applications',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'REASSIGN',
description: 'Ability to reassign applications'
},
{
code: PERMISSIONS.ACTION_SCHEDULE_INTERVIEW,
name: 'Schedule Interviews',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'SCHEDULE_INTERVIEW',
description: 'Ability to schedule interviews'
},
{
code: PERMISSIONS.ACTION_ADD_COMMENTS,
name: 'Add Comments/Notes',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'ADD_COMMENTS',
description: 'Ability to add comments'
},
{
code: PERMISSIONS.ACTION_RANK_APPLICANTS,
name: 'Rank Applicants',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'RANK_APPLICANTS',
description: 'Ability to rank applicants'
},
{
code: PERMISSIONS.ACTION_FINAL_APPROVAL,
name: 'Final Approval',
category: PERMISSION_CATEGORIES.ACTION,
module: 'APPLICATIONS',
type: 'ACTION',
action: 'FINAL_APPROVAL',
description: 'Ability to give final approval'
},
// View Permissions
{
code: PERMISSIONS.VIEW_DETAILS,
name: 'Application Details',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_DETAILS',
description: 'View basic application details'
},
{
code: PERMISSIONS.VIEW_FINANCIAL,
name: 'Financial Information',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_FINANCIAL',
description: 'View financial information'
},
{
code: PERMISSIONS.VIEW_DISCUSSIONS,
name: 'Discussion Notes',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_DISCUSSIONS',
description: 'View internal discussion notes'
},
{
code: PERMISSIONS.VIEW_PROGRESS,
name: 'Progress Tracking',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_PROGRESS',
description: 'View application progress'
},
{
code: PERMISSIONS.VIEW_AUDIT,
name: 'Audit Logs',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_AUDIT',
description: 'View application audit logs'
},
{
code: PERMISSIONS.VIEW_DOCUMENTS,
name: 'All Documents',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_DOCUMENTS',
description: 'View all documents'
},
{
code: PERMISSIONS.VIEW_PERSONAL,
name: 'Personal Information',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_PERSONAL',
description: 'View applicant personal info'
},
{
code: PERMISSIONS.VIEW_BUSINESS,
name: 'Business Details',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_BUSINESS',
description: 'View applicant business details'
},
{
code: PERMISSIONS.VIEW_REPORTS,
name: 'Reports & Analytics',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_REPORTS',
description: 'View application reports'
},
{
code: PERMISSIONS.VIEW_HISTORY,
name: 'Application History',
category: PERMISSION_CATEGORIES.VIEW,
module: 'APPLICATIONS',
type: 'VIEW',
action: 'VIEW_HISTORY',
description: 'View status history'
},
// Stage Permissions
{
code: PERMISSIONS.STAGE_INITIAL_REVIEW,
name: 'Initial Review',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Initial Review stage'
},
{
code: PERMISSIONS.STAGE_FIELD_VERIFICATION,
name: 'Field Verification',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Field Verification stage'
},
{
code: PERMISSIONS.STAGE_LEVEL1_INTERVIEW,
name: 'Level 1 Interview',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Level 1 Interview stage'
},
{
code: PERMISSIONS.STAGE_LEVEL2_INTERVIEW,
name: 'Level 2 Interview',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Level 2 Interview stage'
},
{
code: PERMISSIONS.STAGE_RANKING,
name: 'Ranking & Selection',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Ranking & Selection stage'
},
{
code: PERMISSIONS.STAGE_LEGAL_REVIEW,
name: 'Legal Review',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Legal Review stage'
},
{
code: PERMISSIONS.STAGE_FINANCIAL_REVIEW,
name: 'Financial Review',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Financial Review stage'
},
{
code: PERMISSIONS.STAGE_FINAL_APPROVAL,
name: 'Final Approval',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Final Approval stage'
},
{
code: PERMISSIONS.STAGE_PAYMENT,
name: 'Payment Verification',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Payment Verification stage'
},
{
code: PERMISSIONS.STAGE_ONBOARDING,
name: 'Onboarding',
category: PERMISSION_CATEGORIES.STAGE,
module: 'WORKFLOW',
type: 'STAGE_ACCESS',
action: 'ACCESS',
description: 'Access to Onboarding stage'
}
];
async function seedPermissions() {
console.log('🌱 Seeding permissions...');
try {
await db.sequelize.authenticate();
for (const p of permissionsToSeed) {
await Permission.upsert({
permissionCode: p.code,
permissionName: p.name,
permissionCategory: p.category,
module: p.module,
permissionType: p.type,
action: p.action,
description: p.description
});
}
console.log('✅ Permissions seeded successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seedPermissions();

38
scripts/seed-roles.ts Normal file
View File

@ -0,0 +1,38 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
const { Role } = db;
const rolesToSeed = [
{ roleCode: ROLES.SUPER_ADMIN, roleName: 'Super Admin', category: 'ADMIN', description: 'Full system access' },
{ roleCode: ROLES.DD_ADMIN, roleName: 'DD Admin', category: 'ADMIN', description: 'Dealer Development Admin' },
{ roleCode: ROLES.DD_HEAD, roleName: 'DD Head', category: 'LEVEL_3', description: 'DD Head' },
{ roleCode: ROLES.DD_LEAD, roleName: 'DD Lead', category: 'LEVEL_2', description: 'DD Lead' },
{ roleCode: ROLES.DD_ZM, roleName: 'DD-ZM', category: 'LEVEL_1', description: 'DD Zone Manager' },
{ roleCode: ROLES.ZBH, roleName: 'ZBH', category: 'ZONAL', description: 'Zonal Business Head' },
{ roleCode: ROLES.RBM, roleName: 'RBM', category: 'REGIONAL', description: 'Regional Business Manager' },
{ roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance Department' },
{ roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' },
{ roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' },
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' }
];
async function seedRoles() {
console.log('🌱 Seeding roles...');
try {
await db.sequelize.authenticate();
for (const r of rolesToSeed) {
await Role.upsert(r);
}
console.log('✅ Roles seeded successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seedRoles();

52
scripts/seed-users.ts Normal file
View File

@ -0,0 +1,52 @@
import 'dotenv/config';
import bcrypt from 'bcryptjs';
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
const { User } = db;
async function seedUsers() {
console.log('🌱 Seeding users...');
try {
await db.sequelize.authenticate();
const hashedPassword = await bcrypt.hash('Admin@123', 10);
const usersToSeed = [
{
email: 'admin@royalenfield.com',
fullName: 'Super Admin',
password: hashedPassword,
roleCode: ROLES.SUPER_ADMIN,
status: 'active'
},
{
email: 'zm@royalenfield.com',
fullName: 'Zone Manager',
password: hashedPassword,
roleCode: ROLES.DD_ZM,
status: 'active'
},
{
email: 'dealer@example.com',
fullName: 'Amit Sharma',
password: hashedPassword,
roleCode: ROLES.DEALER,
status: 'active',
isExternal: true
}
];
for (const u of usersToSeed) {
await User.upsert(u);
}
console.log('✅ Users seeded successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seedUsers();

View File

@ -1,160 +0,0 @@
/**
* Database Seeding Script
* Adds initial test data to the database
*
* Run: node scripts/seed.js
*/
require('dotenv').config();
const bcrypt = require('bcryptjs');
const { query } = require('../config/database');
async function seedDatabase() {
console.log('🌱 Starting database seeding...\n');
try {
// 1. Seed regions
console.log('Adding regions...');
const regions = ['East', 'West', 'North', 'South', 'Central'];
for (const region of regions) {
await query(
`INSERT INTO master_regions (region_name) VALUES ($1) ON CONFLICT (region_name) DO NOTHING`,
[region]
);
}
console.log('✅ Regions added\n');
// 2. Seed zones
console.log('Adding zones...');
const zones = [
{ region: 'West', name: 'Mumbai Zone', code: 'MUM-01' },
{ region: 'West', name: 'Pune Zone', code: 'PUN-01' },
{ region: 'North', name: 'Delhi Zone', code: 'DEL-01' },
{ region: 'South', name: 'Bangalore Zone', code: 'BLR-01' },
{ region: 'East', name: 'Kolkata Zone', code: 'KOL-01' },
];
for (const zone of zones) {
const regionResult = await query('SELECT id FROM master_regions WHERE region_name = $1', [zone.region]);
if (regionResult.rows.length > 0) {
await query(
`INSERT INTO master_zones (region_id, zone_name, zone_code)
VALUES ($1, $2, $3) ON CONFLICT (zone_code) DO NOTHING`,
[regionResult.rows[0].id, zone.name, zone.code]
);
}
}
console.log('✅ Zones added\n');
// 3. Seed users
console.log('Adding users...');
const hashedPassword = await bcrypt.hash('Password@123', 10);
const users = [
{ email: 'admin@royalenfield.com', name: 'Super Admin', role: 'Super Admin', region: null, zone: null },
{ email: 'ddlead@royalenfield.com', name: 'DD Lead', role: 'DD Lead', region: 'West', zone: null },
{ email: 'ddhead@royalenfield.com', name: 'DD Head', role: 'DD Head', region: 'West', zone: null },
{ email: 'nbh@royalenfield.com', name: 'NBH', role: 'NBH', region: null, zone: null },
{ email: 'finance@royalenfield.com', name: 'Finance Admin', role: 'Finance', region: null, zone: null },
{ email: 'legal@royalenfield.com', name: 'Legal Admin', role: 'Legal Admin', region: null, zone: null },
{ email: 'dd@royalenfield.com', name: 'DD Mumbai', role: 'DD', region: 'West', zone: 'Mumbai Zone' },
{ email: 'rbm@royalenfield.com', name: 'RBM West', role: 'RBM', region: 'West', zone: 'Mumbai Zone' },
{ email: 'zbh@royalenfield.com', name: 'ZBH Mumbai', role: 'ZBH', region: 'West', zone: 'Mumbai Zone' },
{ email: 'dealer@example.com', name: 'Amit Sharma', role: 'Dealer', region: 'West', zone: 'Mumbai Zone' },
];
for (const user of users) {
const result = await query(
`INSERT INTO users (email, password, full_name, role, region, zone, phone)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (email) DO NOTHING
RETURNING id`,
[user.email, hashedPassword, user.name, user.role, user.region, user.zone, '+91-9876543210']
);
if (result.rows.length > 0) {
console.log(` Added: ${user.email} (${user.role})`);
}
}
console.log('✅ Users added\n');
// 4. Seed outlets for dealer
console.log('Adding outlets...');
const dealerResult = await query('SELECT id FROM users WHERE email = $1', ['dealer@example.com']);
if (dealerResult.rows.length > 0) {
const dealerId = dealerResult.rows[0].id;
const outlets = [
{
code: 'DL-MH-001',
name: 'Royal Enfield Mumbai',
type: 'Dealership',
address: 'Plot No. 45, Linking Road, Bandra West',
city: 'Mumbai',
state: 'Maharashtra',
lat: 19.0596,
lon: 72.8295
},
{
code: 'ST-MH-002',
name: 'Royal Enfield Andheri Studio',
type: 'Studio',
address: 'Shop 12, Phoenix Market City, Kurla',
city: 'Mumbai',
state: 'Maharashtra',
lat: 19.0822,
lon: 72.8912
},
{
code: 'DL-MH-003',
name: 'Royal Enfield Thane Dealership',
type: 'Dealership',
address: 'Eastern Express Highway, Thane West',
city: 'Thane',
state: 'Maharashtra',
lat: 19.2183,
lon: 72.9781
},
{
code: 'ST-MH-004',
name: 'Royal Enfield Pune Studio',
type: 'Studio',
address: 'FC Road, Deccan Gymkhana',
city: 'Pune',
state: 'Maharashtra',
lat: 18.5204,
lon: 73.8567
}
];
for (const outlet of outlets) {
await query(
`INSERT INTO outlets
(dealer_id, code, name, type, address, city, state, status, established_date, latitude, longitude)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'Active', '2020-01-15', $8, $9)
ON CONFLICT (code) DO NOTHING`,
[dealerId, outlet.code, outlet.name, outlet.type, outlet.address, outlet.city, outlet.state, outlet.lat, outlet.lon]
);
}
console.log('✅ Outlets added\n');
}
console.log('✅ Database seeding completed successfully!');
console.log('\n📝 Test Credentials:');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Email: admin@royalenfield.com');
console.log('Email: dealer@example.com');
console.log('Email: finance@royalenfield.com');
console.log('Email: ddlead@royalenfield.com');
console.log('\nPassword (all users): Password@123');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seedDatabase();

View File

@ -0,0 +1,314 @@
'use strict';
const { v4: uuidv4 } = require('uuid');
// Zone Definitions
const ZONES_DATA = [
{ code: 'NZ', name: 'North Zone', description: 'Northern India including NCR', states: ['JAMMU & KASHMIR', 'LADAKH', 'HIMACHAL PRADESH', 'PUNJAB', 'CHANDIGARH', 'UTTARAKHAND', 'HARYANA', 'DELHI', 'RAJASTHAN', 'UTTAR PRADESH'] },
{ code: 'SZ', name: 'South Zone', description: 'Southern India', states: ['KARNATAKA', 'TELANGANA', 'ANDHRA PRADESH', 'TAMIL NADU', 'KERALA', 'PUDUCHERRY', 'LAKSHADWEEP', 'ANDAMAN & NICOBAR'] },
{ code: 'EZ', name: 'East Zone', description: 'Eastern and North-Eastern India', states: ['BIHAR', 'JHARKHAND', 'ODISHA', 'WEST BENGAL', 'SIKKIM', 'ASSAM', 'MEGHALAYA', 'ARUNACHAL PRADESH', 'NAGALAND', 'MANIPUR', 'MIZORAM', 'TRIPURA'] },
{ code: 'WZ', name: 'West Zone', description: 'Western India', states: ['GUJARAT', 'MAHARASHTRA', 'GOA', 'DADRA & NAGAR HAVELI', 'DAMAN & DIU'] },
{ code: 'CZ', name: 'Central Zone', description: 'Central India', states: ['MADHYA PRADESH', 'CHHATTISGARH'] }
];
// Raw State Data
const STATES_DATA = [
{ id: 1, name: 'ANDHRA PRADESH', code: 'AP', country_id: 105 },
{ id: 2, name: 'ASSAM', code: 'AS', country_id: 105 },
{ id: 3, name: 'ARUNACHAL PRADESH', code: 'AR', country_id: 105 },
{ id: 4, name: 'BIHAR', code: 'BR', country_id: 105 },
{ id: 5, name: 'GUJARAT', code: 'GJ', country_id: 105 },
{ id: 6, name: 'HARYANA', code: 'HR', country_id: 105 },
{ id: 7, name: 'HIMACHAL PRADESH', code: 'HP', country_id: 105 },
{ id: 8, name: 'JAMMU & KASHMIR', code: 'JK', country_id: 105 },
{ id: 9, name: 'KARNATAKA', code: 'KA', country_id: 105 },
{ id: 10, name: 'KERALA', code: 'KL', country_id: 105 },
{ id: 11, name: 'MADHYA PRADESH', code: 'MP', country_id: 105 },
{ id: 12, name: 'MAHARASHTRA', code: 'MH', country_id: 105 },
{ id: 13, name: 'MANIPUR', code: 'MN', country_id: 105 },
{ id: 14, name: 'MEGHALAYA', code: 'ML', country_id: 105 },
{ id: 15, name: 'MIZORAM', code: 'MZ', country_id: 105 },
{ id: 16, name: 'NAGALAND', code: 'NL', country_id: 105 },
{ id: 17, name: 'ODISHA', code: 'OD', country_id: 105 },
{ id: 18, name: 'PUNJAB', code: 'PB', country_id: 105 },
{ id: 19, name: 'RAJASTHAN', code: 'RJ', country_id: 105 },
{ id: 20, name: 'SIKKIM', code: 'SK', country_id: 105 },
{ id: 21, name: 'TAMIL NADU', code: 'TN', country_id: 105 },
{ id: 22, name: 'TRIPURA', code: 'TR', country_id: 105 },
{ id: 23, name: 'UTTAR PRADESH', code: 'UP', country_id: 105 },
{ id: 24, name: 'WEST BENGAL', code: 'WB', country_id: 105 },
{ id: 25, name: 'DELHI', code: 'DL', country_id: 105 },
{ id: 26, name: 'GOA', code: 'GA', country_id: 105 },
{ id: 27, name: 'PUDUCHERRY', code: 'PY', country_id: 105 },
{ id: 28, name: 'LAKSHADWEEP', code: 'LD', country_id: 105 },
{ id: 29, name: 'DAMAN & DIU', code: 'DD', country_id: 105 },
{ id: 30, name: 'DADRA & NAGAR HAVELI', code: 'DN', country_id: 105 },
{ id: 31, name: 'CHANDIGARH', code: 'CH', country_id: 105 },
{ id: 32, name: 'ANDAMAN & NICOBAR', code: 'AN', country_id: 105 },
{ id: 33, name: 'UTTARAKHAND', code: 'UK', country_id: 105 },
{ id: 34, name: 'JHARKHAND', code: 'JH', country_id: 105 },
{ id: 35, name: 'CHHATTISGARH', code: 'CG', country_id: 105 },
{ id: 36, name: 'TELANGANA', code: 'TG', country_id: 105 },
{ id: 37, name: 'LADAKH', code: 'LA', country_id: 105 }
];
// Raw City Data
const CITIES_DATA = [
// Bihar (ID 4)
{ name: 'Araria', state_id: 4 }, { name: 'Arwal', state_id: 4 }, { name: 'Aurangabad', state_id: 4 }, { name: 'Banka', state_id: 4 }, { name: 'Begusarai', state_id: 4 }, { name: 'Bhagalpur', state_id: 4 }, { name: 'Bhojpur', state_id: 4 },
{ name: 'Buxar', state_id: 4 }, { name: 'Darbhanga', state_id: 4 }, { name: 'East Champaran', state_id: 4 }, { name: 'Gaya', state_id: 4 }, { name: 'Gopalganj', state_id: 4 }, { name: 'Jamui', state_id: 4 }, { name: 'Jehanabad', state_id: 4 },
{ name: 'Kaimur', state_id: 4 }, { name: 'Katihar', state_id: 4 }, { name: 'Khagaria', state_id: 4 }, { name: 'Kishanganj', state_id: 4 }, { name: 'Lakhisarai', state_id: 4 }, { name: 'Madhepura', state_id: 4 }, { name: 'Madhubani', state_id: 4 },
{ name: 'Munger', state_id: 4 }, { name: 'Muzaffarpur', state_id: 4 }, { name: 'Nalanda', state_id: 4 }, { name: 'Nawada', state_id: 4 }, { name: 'Patna', state_id: 4 }, { name: 'Purnia', state_id: 4 }, { name: 'Rohtas', state_id: 4 },
{ name: 'Saharsa', state_id: 4 }, { name: 'Samastipur', state_id: 4 }, { name: 'Saran', state_id: 4 }, { name: 'Sheikhpura', state_id: 4 }, { name: 'Sheohar', state_id: 4 }, { name: 'Sitamarhi', state_id: 4 }, { name: 'Siwan', state_id: 4 },
{ name: 'Supaul', state_id: 4 }, { name: 'Vaishali', state_id: 4 }, { name: 'West Champaran', state_id: 4 },
// Chhattisgarh (ID 35)
{ name: 'Balod', state_id: 35 }, { name: 'Baloda Bazar', state_id: 35 }, { name: 'Balrampur', state_id: 35 }, { name: 'Bastar', state_id: 35 }, { name: 'Bemetara', state_id: 35 }, { name: 'Bijapur', state_id: 35 }, { name: 'Bilaspur', state_id: 35 },
{ name: 'Dantewada', state_id: 35 }, { name: 'Dhamtari', state_id: 35 }, { name: 'Durg', state_id: 35 }, { name: 'Gariaband', state_id: 35 }, { name: 'Gaurela-Pendra-Marwahi', state_id: 35 }, { name: 'Janjgir-Champa', state_id: 35 }, { name: 'Jashpur', state_id: 35 },
{ name: 'Kabirdham', state_id: 35 }, { name: 'Kanker', state_id: 35 }, { name: 'Khairagarh-Chhuikhadan-Gandai', state_id: 35 }, { name: 'Kondagaon', state_id: 35 }, { name: 'Korba', state_id: 35 }, { name: 'Koriya', state_id: 35 }, { name: 'Mahasamund', state_id: 35 },
{ name: 'Manendragarh-Chirmiri-Bharatpur', state_id: 35 }, { name: 'Mohla-Manpur-Ambagarh Chowki', state_id: 35 }, { name: 'Mungeli', state_id: 35 }, { name: 'Narayanpur', state_id: 35 }, { name: 'Raigarh', state_id: 35 }, { name: 'Raipur', state_id: 35 },
{ name: 'Rajnandgaon', state_id: 35 }, { name: 'Sakti', state_id: 35 }, { name: 'Sarangarh-Bilaigarh', state_id: 35 }, { name: 'Sukma', state_id: 35 }, { name: 'Surajpur', state_id: 35 }, { name: 'Surguja', state_id: 35 },
{ name: 'Diu', state_id: 29 }, { name: 'Daman', state_id: 29 }, { name: 'Central Delhi', state_id: 25 }, { name: 'East Delhi', state_id: 25 },
{ name: 'New Delhi', state_id: 25 }, { name: 'North Delhi', state_id: 25 }, { name: 'North East Delhi', state_id: 25 }, { name: 'North West Delhi', state_id: 25 }, { name: 'South Delhi', state_id: 25 }, { name: 'South West Delhi', state_id: 25 },
{ name: 'West Delhi', state_id: 25 }, { name: 'North Goa', state_id: 26 }, { name: 'South Goa', state_id: 26 },
// Gujarat (ID 5)
{ name: 'Ahmedabad', state_id: 5 }, { name: 'Amreli', state_id: 5 }, { name: 'Anand', state_id: 5 }, { name: 'Aravalli', state_id: 5 }, { name: 'Banaskantha', state_id: 5 }, { name: 'Bharuch', state_id: 5 }, { name: 'Bhavnagar', state_id: 5 },
{ name: 'Botad', state_id: 5 }, { name: 'Chhota Udaipur', state_id: 5 }, { name: 'Dahod', state_id: 5 }, { name: 'Dang', state_id: 5 }, { name: 'Devbhoomi Dwarka', state_id: 5 }, { name: 'Gandhinagar', state_id: 5 }, { name: 'Gir Somnath', state_id: 5 },
{ name: 'Jamnagar', state_id: 5 }, { name: 'Junagadh', state_id: 5 }, { name: 'Kheda', state_id: 5 }, { name: 'Kutch', state_id: 5 }, { name: 'Mahisagar', state_id: 5 }, { name: 'Mehsana', state_id: 5 }, { name: 'Morbi', state_id: 5 },
{ name: 'Narmada', state_id: 5 }, { name: 'Navsari', state_id: 5 }, { name: 'Panchmahal', state_id: 5 }, { name: 'Patan', state_id: 5 }, { name: 'Porbandar', state_id: 5 }, { name: 'Rajkot', state_id: 5 }, { name: 'Sabarkantha', state_id: 5 },
{ name: 'Surat', state_id: 5 }, { name: 'Surendranagar', state_id: 5 }, { name: 'Tapi', state_id: 5 }, { name: 'Vadodara', state_id: 5 }, { name: 'Valsad', state_id: 5 },
// Haryana (ID 6)
{ name: 'Ambala', state_id: 6 }, { name: 'Bhiwani', state_id: 6 }, { name: 'Charkhi Dadri', state_id: 6 }, { name: 'Faridabad', state_id: 6 }, { name: 'Fatehabad', state_id: 6 }, { name: 'Gurugram', state_id: 6 }, { name: 'Hisar', state_id: 6 },
{ name: 'Jhajjar', state_id: 6 }, { name: 'Jind', state_id: 6 }, { name: 'Kaithal', state_id: 6 }, { name: 'Karnal', state_id: 6 }, { name: 'Kurukshetra', state_id: 6 }, { name: 'Mahendragarh', state_id: 6 }, { name: 'Nuh', state_id: 6 },
{ name: 'Palwal', state_id: 6 }, { name: 'Panchkula', state_id: 6 }, { name: 'Panipat', state_id: 6 }, { name: 'Rewari', state_id: 6 }, { name: 'Rohtak', state_id: 6 }, { name: 'Sirsa', state_id: 6 }, { name: 'Sonipat', state_id: 6 },
{ name: 'Yamunanagar', state_id: 6 },
{ name: 'Bilaspur', state_id: 7 }, { name: 'Chamba', state_id: 7 }, { name: 'Hamirpur', state_id: 7 }, { name: 'Kangra', state_id: 7 }, { name: 'Kinnaur', state_id: 7 }, { name: 'Kulu', state_id: 7 }, { name: 'Lahaul and Spiti', state_id: 7 },
{ name: 'Mandi', state_id: 7 }, { name: 'Shimla', state_id: 7 }, { name: 'Sirmaur', state_id: 7 }, { name: 'Solan', state_id: 7 }, { name: 'Una', state_id: 7 }, { name: 'Anantnag', state_id: 8 }, { name: 'Badgam', state_id: 8 },
{ name: 'Bandipore', state_id: 8 }, { name: 'Baramula', state_id: 8 }, { name: 'Doda', state_id: 8 }, { name: 'Jammu', state_id: 8 }, { name: 'Kargil', state_id: 37 }, { name: 'Kathua', state_id: 8 }, { name: 'Kupwara', state_id: 8 },
{ name: 'Leh', state_id: 37 }, { name: 'Poonch', state_id: 8 }, { name: 'Pulwama', state_id: 8 }, { name: 'Rajauri', state_id: 8 }, { name: 'Srinagar', state_id: 8 }, { name: 'Samba', state_id: 8 }, { name: 'Udhampur', state_id: 8 },
{ name: 'Bokaro', state_id: 34 }, { name: 'Chatra', state_id: 34 }, { name: 'Deoghar', state_id: 34 }, { name: 'Dhanbad', state_id: 34 }, { name: 'Dumka', state_id: 34 }, { name: 'Purba Singhbhum', state_id: 34 }, { name: 'Garhwa', state_id: 34 },
{ name: 'Giridih', state_id: 34 }, { name: 'Godda', state_id: 34 }, { name: 'Gumla', state_id: 34 }, { name: 'Hazaribagh', state_id: 34 }, { name: 'Koderma', state_id: 34 }, { name: 'Lohardaga', state_id: 34 }, { name: 'Pakur', state_id: 34 },
{ name: 'Palamu', state_id: 34 }, { name: 'Ranchi', state_id: 34 }, { name: 'Sahibganj', state_id: 34 }, { name: 'Seraikela and Kharsawan', state_id: 34 }, { name: 'Pashchim Singhbhum', state_id: 34 }, { name: 'Ramgarh', state_id: 34 },
{ name: 'Bidar', state_id: 9 }, { name: 'Belgaum', state_id: 9 }, { name: 'Bijapur', state_id: 9 }, { name: 'Bagalkot', state_id: 9 }, { name: 'Bellary', state_id: 9 }, { name: 'Bangalore Rural District', state_id: 9 },
{ name: 'Bangalore Urban District', state_id: 9 }, { name: 'Chamarajnagar', state_id: 9 }, { name: 'Chikmagalur', state_id: 9 }, { name: 'Chitradurga', state_id: 9 }, { name: 'Davanagere', state_id: 9 }, { name: 'Dharwad', state_id: 9 },
{ name: 'Dakshina Kannada', state_id: 9 }, { name: 'Gadag', state_id: 9 }, { name: 'Gulbarga', state_id: 9 }, { name: 'Hassan', state_id: 9 }, { name: 'Haveri District', state_id: 9 }, { name: 'Kodagu', state_id: 9 }, { name: 'Kolar', state_id: 9 },
{ name: 'Koppal', state_id: 9 }, { name: 'Mandya', state_id: 9 }, { name: 'Mysore', state_id: 9 }, { name: 'Raichur', state_id: 9 }, { name: 'Shimoga', state_id: 9 }, { name: 'Tumkur', state_id: 9 }, { name: 'Udupi', state_id: 9 },
{ name: 'Uttara Kannada', state_id: 9 }, { name: 'Ramanagara', state_id: 9 }, { name: 'Chikballapur', state_id: 9 }, { name: 'Yadagiri', state_id: 9 }, { name: 'Vijayanagara', state_id: 9 },
{ name: 'Alappuzha', state_id: 10 }, { name: 'Ernakulam', state_id: 10 },
{ name: 'Idukki', state_id: 10 }, { name: 'Kollam', state_id: 10 }, { name: 'Kannur', state_id: 10 }, { name: 'Kasaragod', state_id: 10 }, { name: 'Kottayam', state_id: 10 }, { name: 'Kozhikode', state_id: 10 }, { name: 'Malappuram', state_id: 10 },
{ name: 'Thrissur', state_id: 10 }, { name: 'Thiruvananthapuram', state_id: 10 }, { name: 'Wayanad', state_id: 10 },
// Madhya Pradesh (ID 11)
{ name: 'Agar Malwa', state_id: 11 }, { name: 'Alirajpur', state_id: 11 }, { name: 'Anuppur', state_id: 11 }, { name: 'Ashok Nagar', state_id: 11 }, { name: 'Balaghat', state_id: 11 }, { name: 'Barwani', state_id: 11 }, { name: 'Betul', state_id: 11 },
{ name: 'Bhind', state_id: 11 }, { name: 'Bhopal', state_id: 11 }, { name: 'Burhanpur', state_id: 11 }, { name: 'Chhatarpur', state_id: 11 }, { name: 'Chhindwara', state_id: 11 }, { name: 'Damoh', state_id: 11 }, { name: 'Datia', state_id: 11 },
{ name: 'Dewas', state_id: 11 }, { name: 'Dhar', state_id: 11 }, { name: 'Dindori', state_id: 11 }, { name: 'Guna', state_id: 11 }, { name: 'Gwalior', state_id: 11 }, { name: 'Harda', state_id: 11 }, { name: 'Hoshangabad', state_id: 11 }, // Narmadapuram
{ name: 'Indore', state_id: 11 }, { name: 'Jabalpur', state_id: 11 }, { name: 'Jhabua', state_id: 11 }, { name: 'Katni', state_id: 11 }, { name: 'Khandwa', state_id: 11 }, { name: 'Khargone', state_id: 11 }, { name: 'Maihar', state_id: 11 },
{ name: 'Mandla', state_id: 11 }, { name: 'Mandsaur', state_id: 11 }, { name: 'Mauganj', state_id: 11 }, { name: 'Morena', state_id: 11 }, { name: 'Narsinghpur', state_id: 11 }, { name: 'Neemuch', state_id: 11 }, { name: 'Niwari', state_id: 11 },
{ name: 'Pandhurna', state_id: 11 }, { name: 'Panna', state_id: 11 }, { name: 'Raisen', state_id: 11 }, { name: 'Rajgarh', state_id: 11 }, { name: 'Ratlam', state_id: 11 }, { name: 'Rewa', state_id: 11 }, { name: 'Sagar', state_id: 11 },
{ name: 'Satna', state_id: 11 }, { name: 'Sehore', state_id: 11 }, { name: 'Seoni', state_id: 11 }, { name: 'Shahdol', state_id: 11 }, { name: 'Shajapur', state_id: 11 }, { name: 'Sheopur', state_id: 11 }, { name: 'Shivpuri', state_id: 11 },
{ name: 'Sidhi', state_id: 11 }, { name: 'Singrauli', state_id: 11 }, { name: 'Tikamgarh', state_id: 11 }, { name: 'Ujjain', state_id: 11 }, { name: 'Umaria', state_id: 11 }, { name: 'Vidisha', state_id: 11 },
{ name: 'Ahmednagar', state_id: 12 }, { name: 'Akola', state_id: 12 }, { name: 'Amrawati', state_id: 12 }, { name: 'Aurangabad', state_id: 12 }, { name: 'Bhandara', state_id: 12 }, { name: 'Beed', state_id: 12 }, { name: 'Buldhana', state_id: 12 },
{ name: 'Chandrapur', state_id: 12 }, { name: 'Dhule', state_id: 12 }, { name: 'Gadchiroli', state_id: 12 }, { name: 'Gondiya', state_id: 12 }, { name: 'Hingoli', state_id: 12 }, { name: 'Jalgaon', state_id: 12 }, { name: 'Jalna', state_id: 12 },
{ name: 'Kolhapur', state_id: 12 }, { name: 'Latur', state_id: 12 }, { name: 'Mumbai City', state_id: 12 }, { name: 'Mumbai suburban', state_id: 12 }, { name: 'Nandurbar', state_id: 12 }, { name: 'Nanded', state_id: 12 }, { name: 'Nagpur', state_id: 12 },
{ name: 'Nashik', state_id: 12 }, { name: 'Osmanabad', state_id: 12 }, { name: 'Parbhani', state_id: 12 }, { name: 'Pune', state_id: 12 }, { name: 'Raigad', state_id: 12 }, { name: 'Ratnagiri', state_id: 12 }, { name: 'Sindhudurg', state_id: 12 },
{ name: 'Sangli', state_id: 12 }, { name: 'Solapur', state_id: 12 }, { name: 'Satara', state_id: 12 }, { name: 'Thane', state_id: 12 }, { name: 'Wardha', state_id: 12 }, { name: 'Washim', state_id: 12 }, { name: 'Yavatmal', state_id: 12 },
// Manipur (ID 13)
{ name: 'Bishnupur', state_id: 13 }, { name: 'Chandel', state_id: 13 }, { name: 'Churachandpur', state_id: 13 }, { name: 'Imphal East', state_id: 13 }, { name: 'Imphal West', state_id: 13 }, { name: 'Jiribam', state_id: 13 }, { name: 'Kakching', state_id: 13 },
{ name: 'Kamjong', state_id: 13 }, { name: 'Kangpokpi', state_id: 13 }, { name: 'Noney', state_id: 13 }, { name: 'Pherzawl', state_id: 13 }, { name: 'Senapati', state_id: 13 }, { name: 'Tamenglong', state_id: 13 }, { name: 'Tengnoupal', state_id: 13 },
{ name: 'Thoubal', state_id: 13 }, { name: 'Ukhrul', state_id: 13 },
// Meghalaya (ID 14)
{ name: 'East Garo Hills', state_id: 14 }, { name: 'East Jaintia Hills', state_id: 14 }, { name: 'East Khasi Hills', state_id: 14 }, { name: 'Eastern West Khasi Hills', state_id: 14 }, { name: 'North Garo Hills', state_id: 14 }, { name: 'Ri-Bhoi', state_id: 14 },
{ name: 'South Garo Hills', state_id: 14 }, { name: 'South West Garo Hills', state_id: 14 }, { name: 'South West Khasi Hills', state_id: 14 }, { name: 'West Garo Hills', state_id: 14 }, { name: 'West Jaintia Hills', state_id: 14 }, { name: 'West Khasi Hills', state_id: 14 },
// Mizoram (ID 15)
{ name: 'Aizawl', state_id: 15 }, { name: 'Champhai', state_id: 15 }, { name: 'Hnahthial', state_id: 15 }, { name: 'Khawzawl', state_id: 15 }, { name: 'Kolasib', state_id: 15 }, { name: 'Lawngtlai', state_id: 15 }, { name: 'Lunglei', state_id: 15 },
{ name: 'Mamit', state_id: 15 }, { name: 'Saiha', state_id: 15 }, { name: 'Saitual', state_id: 15 }, { name: 'Serchhip', state_id: 15 },
// Nagaland (ID 16)
{ name: 'Chumoukedima', state_id: 16 }, { name: 'Dimapur', state_id: 16 }, { name: 'Kiphire', state_id: 16 }, { name: 'Kohima', state_id: 16 }, { name: 'Longleng', state_id: 16 }, { name: 'Mokokchung', state_id: 16 }, { name: 'Mon', state_id: 16 },
{ name: 'Niuland', state_id: 16 }, { name: 'Noklak', state_id: 16 }, { name: 'Peren', state_id: 16 }, { name: 'Phek', state_id: 16 }, { name: 'Shamator', state_id: 16 }, { name: 'Tseminyu', state_id: 16 }, { name: 'Tuensang', state_id: 16 },
{ name: 'Wokha', state_id: 16 }, { name: 'Zunheboto', state_id: 16 },
{ name: 'Angul', state_id: 17 }, { name: 'Boudh', state_id: 17 }, { name: 'Bhadrak', state_id: 17 }, { name: 'Bolangir', state_id: 17 }, { name: 'Bargarh', state_id: 17 }, { name: 'Baleswar', state_id: 17 }, { name: 'Cuttack', state_id: 17 },
{ name: 'Debagarh', state_id: 17 }, { name: 'Dhenkanal', state_id: 17 }, { name: 'Ganjam', state_id: 17 }, { name: 'Gajapati', state_id: 17 }, { name: 'Jharsuguda', state_id: 17 }, { name: 'Jajapur', state_id: 17 }, { name: 'Jagatsinghpur', state_id: 17 },
{ name: 'Khordha', state_id: 17 }, { name: 'Kendujhar', state_id: 17 }, { name: 'Kalahandi', state_id: 17 }, { name: 'Kandhamal', state_id: 17 }, { name: 'Koraput', state_id: 17 }, { name: 'Kendrapara', state_id: 17 }, { name: 'Malkangiri', state_id: 17 },
{ name: 'Mayurbhanj', state_id: 17 }, { name: 'Nabarangpur', state_id: 17 }, { name: 'Nuapada', state_id: 17 }, { name: 'Nayagarh', state_id: 17 }, { name: 'Puri', state_id: 17 }, { name: 'Rayagada', state_id: 17 }, { name: 'Sambalpur', state_id: 17 },
{ name: 'Subarnapur', state_id: 17 }, { name: 'Sundargarh', state_id: 17 }, { name: 'Karaikal', state_id: 27 }, { name: 'Mahe', state_id: 27 }, { name: 'Puducherry', state_id: 27 }, { name: 'Yanam', state_id: 27 },
// Punjab (ID 18)
{ name: 'Amritsar', state_id: 18 }, { name: 'Barnala', state_id: 18 }, { name: 'Bathinda', state_id: 18 }, { name: 'Faridkot', state_id: 18 }, { name: 'Fatehgarh Sahib', state_id: 18 }, { name: 'Fazilka', state_id: 18 }, { name: 'Ferozepur', state_id: 18 },
{ name: 'Gurdaspur', state_id: 18 }, { name: 'Hoshiarpur', state_id: 18 }, { name: 'Jalandhar', state_id: 18 }, { name: 'Kapurthala', state_id: 18 }, { name: 'Ludhiana', state_id: 18 }, { name: 'Malerkotla', state_id: 18 }, { name: 'Mansa', state_id: 18 },
{ name: 'Moga', state_id: 18 }, { name: 'Mohali', state_id: 18 }, { name: 'Muktsar', state_id: 18 }, { name: 'Nawanshahr', state_id: 18 }, { name: 'Pathankot', state_id: 18 }, { name: 'Patiala', state_id: 18 }, { name: 'Rupnagar', state_id: 18 },
{ name: 'Sangrur', state_id: 18 }, { name: 'Tarn Taran', state_id: 18 },
// Rajasthan (ID 19)
{ name: 'Ajmer', state_id: 19 }, { name: 'Alwar', state_id: 19 }, { name: 'Balotra', state_id: 19 }, { name: 'Banswara', state_id: 19 }, { name: 'Baran', state_id: 19 }, { name: 'Barmer', state_id: 19 }, { name: 'Beawar', state_id: 19 },
{ name: 'Bharatpur', state_id: 19 }, { name: 'Bhilwara', state_id: 19 }, { name: 'Bikaner', state_id: 19 }, { name: 'Bundi', state_id: 19 }, { name: 'Chittorgarh', state_id: 19 }, { name: 'Churu', state_id: 19 }, { name: 'Dausa', state_id: 19 },
{ name: 'Deeg', state_id: 19 }, { name: 'Didwana-Kuchaman', state_id: 19 }, { name: 'Dholpur', state_id: 19 }, { name: 'Dungarpur', state_id: 19 }, { name: 'Hanumangarh', state_id: 19 }, { name: 'Jaipur', state_id: 19 }, { name: 'Jaisalmer', state_id: 19 },
{ name: 'Jalore', state_id: 19 }, { name: 'Jhalawar', state_id: 19 }, { name: 'Jhunjhunu', state_id: 19 }, { name: 'Jodhpur', state_id: 19 }, { name: 'Karauli', state_id: 19 }, { name: 'Khairthal-Tijara', state_id: 19 }, { name: 'Kota', state_id: 19 },
{ name: 'Kotputli-Behror', state_id: 19 }, { name: 'Nagaur', state_id: 19 }, { name: 'Pali', state_id: 19 }, { name: 'Phalodi', state_id: 19 }, { name: 'Pratapgarh', state_id: 19 }, { name: 'Rajsamand', state_id: 19 }, { name: 'Salumbar', state_id: 19 },
{ name: 'Sawai Madhopur', state_id: 19 }, { name: 'Sikar', state_id: 19 }, { name: 'Sirohi', state_id: 19 }, { name: 'Sri Ganganagar', state_id: 19 }, { name: 'Tonk', state_id: 19 }, { name: 'Udaipur', state_id: 19 },
// Sikkim (ID 20)
{ name: 'Gangtok', state_id: 20 }, { name: 'Gyalshing', state_id: 20 }, { name: 'Mangan', state_id: 20 }, { name: 'Namchi', state_id: 20 }, { name: 'Pakyong', state_id: 20 }, { name: 'Soreng', state_id: 20 },
// Tamil Nadu (ID 21)
{ name: 'Ariyalur', state_id: 21 }, { name: 'Chengalpattu', state_id: 21 }, { name: 'Chennai', state_id: 21 }, { name: 'Coimbatore', state_id: 21 }, { name: 'Cuddalore', state_id: 21 }, { name: 'Dharmapuri', state_id: 21 }, { name: 'Dindigul', state_id: 21 },
{ name: 'Erode', state_id: 21 }, { name: 'Kallakurichi', state_id: 21 }, { name: 'Kanchipuram', state_id: 21 }, { name: 'Kanyakumari', state_id: 21 }, { name: 'Karur', state_id: 21 }, { name: 'Krishnagiri', state_id: 21 }, { name: 'Madurai', state_id: 21 },
{ name: 'Mayiladuthurai', state_id: 21 }, { name: 'Nagapattinam', state_id: 21 }, { name: 'Namakkal', state_id: 21 }, { name: 'Nilgiris', state_id: 21 }, { name: 'Perambalur', state_id: 21 }, { name: 'Pudukkottai', state_id: 21 }, { name: 'Ramanathapuram', state_id: 21 },
{ name: 'Ranipet', state_id: 21 }, { name: 'Salem', state_id: 21 }, { name: 'Sivagangai', state_id: 21 }, { name: 'Tenkasi', state_id: 21 }, { name: 'Thanjavur', state_id: 21 }, { name: 'Theni', state_id: 21 }, { name: 'Thoothukudi', state_id: 21 },
{ name: 'Tiruchirappalli', state_id: 21 }, { name: 'Tirunelveli', state_id: 21 }, { name: 'Tirupattur', state_id: 21 }, { name: 'Tiruppur', state_id: 21 }, { name: 'Tiruvallur', state_id: 21 }, { name: 'Tiruvannamalai', state_id: 21 },
{ name: 'Tiruvarur', state_id: 21 }, { name: 'Vellore', state_id: 21 }, { name: 'Viluppuram', state_id: 21 }, { name: 'Virudhunagar', state_id: 21 },
{ name: 'Bageshwar', state_id: 33 }, { name: 'Chamoli', state_id: 33 }, { name: 'Champawat', state_id: 33 }, { name: 'Dehradun', state_id: 33 }, { name: 'Haridwar', state_id: 33 }, { name: 'Nainital', state_id: 33 }, { name: 'Pauri Garhwal', state_id: 33 },
{ name: 'Pithoragharh', state_id: 33 }, { name: 'Rudraprayag', state_id: 33 }, { name: 'Tehri Garhwal', state_id: 33 }, { name: 'Udham Singh Nagar', state_id: 33 }, { name: 'Uttarkashi', state_id: 33 }, { name: 'Agra', state_id: 23 },
// Uttar Pradesh (ID 23)
{ name: 'Agra', state_id: 23 }, { name: 'Aligarh', state_id: 23 }, { name: 'Ambedkar Nagar', state_id: 23 }, { name: 'Amethi', state_id: 23 }, { name: 'Amroha', state_id: 23 }, { name: 'Auraiya', state_id: 23 }, { name: 'Ayodhya', state_id: 23 },
{ name: 'Azamgarh', state_id: 23 }, { name: 'Badaun', state_id: 23 }, { name: 'Baghpat', state_id: 23 }, { name: 'Bahraich', state_id: 23 }, { name: 'Ballia', state_id: 23 }, { name: 'Balrampur', state_id: 23 }, { name: 'Banda', state_id: 23 },
{ name: 'Barabanki', state_id: 23 }, { name: 'Bareilly', state_id: 23 }, { name: 'Basti', state_id: 23 }, { name: 'Bhadohi', state_id: 23 }, { name: 'Bijnor', state_id: 23 }, { name: 'Bulandshahr', state_id: 23 }, { name: 'Chandauli', state_id: 23 },
{ name: 'Chitrakoot', state_id: 23 }, { name: 'Deoria', state_id: 23 }, { name: 'Etah', state_id: 23 }, { name: 'Etawah', state_id: 23 }, { name: 'Farrukhabad', state_id: 23 }, { name: 'Fatehpur', state_id: 23 }, { name: 'Firozabad', state_id: 23 },
{ name: 'Gautam Buddha Nagar', state_id: 23 }, { name: 'Ghaziabad', state_id: 23 }, { name: 'Ghazipur', state_id: 23 }, { name: 'Gonda', state_id: 23 }, { name: 'Gorakhpur', state_id: 23 }, { name: 'Hamirpur', state_id: 23 }, { name: 'Hapur', state_id: 23 },
{ name: 'Hardoi', state_id: 23 }, { name: 'Hathras', state_id: 23 }, { name: 'Jalaun', state_id: 23 }, { name: 'Jaunpur', state_id: 23 }, { name: 'Jhansi', state_id: 23 }, { name: 'Kannauj', state_id: 23 }, { name: 'Kanpur Dehat', state_id: 23 },
{ name: 'Kanpur Nagar', state_id: 23 }, { name: 'Kasganj', state_id: 23 }, { name: 'Kaushambi', state_id: 23 }, { name: 'Kushinagar', state_id: 23 }, { name: 'Lakhimpur Kheri', state_id: 23 }, { name: 'Lalitpur', state_id: 23 }, { name: 'Lucknow', state_id: 23 },
{ name: 'Maharajganj', state_id: 23 }, { name: 'Mahoba', state_id: 23 }, { name: 'Mainpuri', state_id: 23 }, { name: 'Mathura', state_id: 23 }, { name: 'Mau', state_id: 23 }, { name: 'Meerut', state_id: 23 }, { name: 'Mirzapur', state_id: 23 },
{ name: 'Moradabad', state_id: 23 }, { name: 'Muzaffarnagar', state_id: 23 }, { name: 'Pilibhit', state_id: 23 }, { name: 'Pratapgarh', state_id: 23 }, { name: 'Prayagraj', state_id: 23 }, { name: 'Rae Bareli', state_id: 23 }, { name: 'Rampur', state_id: 23 },
{ name: 'Saharanpur', state_id: 23 }, { name: 'Sambhal', state_id: 23 }, { name: 'Sant Kabir Nagar', state_id: 23 }, { name: 'Shahjahanpur', state_id: 23 }, { name: 'Shamli', state_id: 23 }, { name: 'Shravasti', state_id: 23 }, { name: 'Siddharthnagar', state_id: 23 },
{ name: 'Sitapur', state_id: 23 }, { name: 'Sonbhadra', state_id: 23 }, { name: 'Sultanpur', state_id: 23 }, { name: 'Unnao', state_id: 23 }, { name: 'Varanasi', state_id: 23 },
// Andhra Pradesh (ID 1)
{ name: 'Anantapur', state_id: 1 }, { name: 'Chittoor', state_id: 1 }, { name: 'East Godavari', state_id: 1 }, { name: 'Guntur', state_id: 1 }, { name: 'Krishna', state_id: 1 },
{ name: 'Kurnool', state_id: 1 }, { name: 'Nellore', state_id: 1 }, { name: 'Prakasam', state_id: 1 }, { name: 'Srikakulam', state_id: 1 }, { name: 'Visakhapatnam', state_id: 1 },
{ name: 'Vizianagaram', state_id: 1 }, { name: 'West Godavari', state_id: 1 }, { name: 'YSR Kadapa', state_id: 1 },
// New AP Districts
{ name: 'Parvathipuram Manyam', state_id: 1 }, { name: 'Alluri Sitharama Raju', state_id: 1 }, { name: 'Anakapalli', state_id: 1 }, { name: 'Kakinada', state_id: 1 }, { name: 'Konaseema', state_id: 1 },
{ name: 'Eluru', state_id: 1 }, { name: 'NTR', state_id: 1 }, { name: 'Bapatla', state_id: 1 }, { name: 'Palnadu', state_id: 1 }, { name: 'Nandyal', state_id: 1 },
{ name: 'Sri Sathya Sai', state_id: 1 }, { name: 'Annamayya', state_id: 1 }, { name: 'Tirupati', state_id: 1 },
// Telangana (ID 36)
{ name: 'Adilabad', state_id: 36 }, { name: 'Bhadradri Kothagudem', state_id: 36 }, { name: 'Hyderabad', state_id: 36 }, { name: 'Jagtial', state_id: 36 }, { name: 'Jangaon', state_id: 36 },
{ name: 'Jayashankar Bhupalpally', state_id: 36 }, { name: 'Jogulamba Gadwal', state_id: 36 }, { name: 'Kamareddy', state_id: 36 }, { name: 'Karimnagar', state_id: 36 }, { name: 'Khammam', state_id: 36 },
{ name: 'Komaram Bheem Asifabad', state_id: 36 }, { name: 'Mahabubabad', state_id: 36 }, { name: 'Mahbubnagar', state_id: 36 }, { name: 'Mancherial', state_id: 36 }, { name: 'Medak', state_id: 36 },
{ name: 'Medchal-Malkajgiri', state_id: 36 }, { name: 'Nagarkurnool', state_id: 36 }, { name: 'Nalgonda', state_id: 36 }, { name: 'Nirmal', state_id: 36 }, { name: 'Nizamabad', state_id: 36 },
{ name: 'Peddapalli', state_id: 36 }, { name: 'Rajanna Sircilla', state_id: 36 }, { name: 'Rangareddy', state_id: 36 }, { name: 'Sangareddy', state_id: 36 }, { name: 'Siddipet', state_id: 36 },
{ name: 'Suryapet', state_id: 36 }, { name: 'Vikarabad', state_id: 36 }, { name: 'Wanaparthy', state_id: 36 }, { name: 'Warangal (Rural)', state_id: 36 }, { name: 'Warangal (Urban)', state_id: 36 },
{ name: 'Yadadri Bhuvanagiri', state_id: 36 }, { name: 'Mulugu', state_id: 36 }, { name: 'Narayanpet', state_id: 36 },
// West Bengal (ID 24)
{ name: 'Alipurduar', state_id: 24 }, { name: 'Bankura', state_id: 24 }, { name: 'Birbhum', state_id: 24 }, { name: 'Cooch Behar', state_id: 24 }, { name: 'Dakshin Dinajpur', state_id: 24 }, { name: 'Darjeeling', state_id: 24 }, { name: 'Hooghly', state_id: 24 },
{ name: 'Howrah', state_id: 24 }, { name: 'Jalpaiguri', state_id: 24 }, { name: 'Jhargram', state_id: 24 }, { name: 'Kalimpong', state_id: 24 }, { name: 'Kolkata', state_id: 24 }, { name: 'Malda', state_id: 24 }, { name: 'Murshidabad', state_id: 24 },
{ name: 'Nadia', state_id: 24 }, { name: 'North 24 Parganas', state_id: 24 }, { name: 'Paschim Bardhaman', state_id: 24 }, { name: 'Paschim Medinipur', state_id: 24 }, { name: 'Purba Bardhaman', state_id: 24 }, { name: 'Purba Medinipur', state_id: 24 },
{ name: 'Purulia', state_id: 24 }, { name: 'South 24 Parganas', state_id: 24 }, { name: 'Uttar Dinajpur', state_id: 24 },
// Arunachal Pradesh (ID 3)
{ name: 'Anjaw', state_id: 3 }, { name: 'Changlang', state_id: 3 }, { name: 'Dibang Valley', state_id: 3 }, { name: 'East Kameng', state_id: 3 }, { name: 'East Siang', state_id: 3 }, { name: 'Kamle', state_id: 3 }, { name: 'Kra Daadi', state_id: 3 },
{ name: 'Kurung Kumey', state_id: 3 }, { name: 'Lepa Rada', state_id: 3 }, { name: 'Lohit', state_id: 3 }, { name: 'Longding', state_id: 3 }, { name: 'Lower Dibang Valley', state_id: 3 }, { name: 'Lower Siang', state_id: 3 }, { name: 'Lower Subansiri', state_id: 3 },
{ name: 'Namsai', state_id: 3 }, { name: 'Pakke Kessang', state_id: 3 }, { name: 'Papum Pare', state_id: 3 }, { name: 'Shi Yomi', state_id: 3 }, { name: 'Siang', state_id: 3 }, { name: 'Tawang', state_id: 3 }, { name: 'Tirap', state_id: 3 },
{ name: 'Upper Siang', state_id: 3 }, { name: 'Upper Subansiri', state_id: 3 }, { name: 'West Kameng', state_id: 3 }, { name: 'West Siang', state_id: 3 }, { name: 'Itanagar Capital Complex', state_id: 3 }, { name: 'Bichom', state_id: 3 }, { name: 'Keyi Panyor', state_id: 3 },
// Assam (ID 2)
{ name: 'Baksa', state_id: 2 }, { name: 'Barpeta', state_id: 2 }, { name: 'Bongaigaon', state_id: 2 }, { name: 'Cachar', state_id: 2 }, { name: 'Charaideo', state_id: 2 }, { name: 'Chirang', state_id: 2 }, { name: 'Darrang', state_id: 2 },
{ name: 'Dhemaji', state_id: 2 }, { name: 'Dhubri', state_id: 2 }, { name: 'Dibrugarh', state_id: 2 }, { name: 'Dima Hasao', state_id: 2 }, { name: 'Goalpara', state_id: 2 }, { name: 'Golaghat', state_id: 2 }, { name: 'Hailakandi', state_id: 2 },
{ name: 'Jorhat', state_id: 2 }, { name: 'Kamrup', state_id: 2 }, { name: 'Kamrup Metropolitan', state_id: 2 }, { name: 'Karbi Anglong', state_id: 2 }, { name: 'Karimganj', state_id: 2 }, { name: 'Kokrajhar', state_id: 2 }, { name: 'Lakhimpur', state_id: 2 },
{ name: 'Majuli', state_id: 2 }, { name: 'Morigaon', state_id: 2 }, { name: 'Nagaon', state_id: 2 }, { name: 'Nalbari', state_id: 2 }, { name: 'Sivasagar', state_id: 2 }, { name: 'Sonitpur', state_id: 2 }, { name: 'South Salmara-Mankachar', state_id: 2 },
{ name: 'Tinsukia', state_id: 2 }, { name: 'Udalguri', state_id: 2 }, { name: 'West Karbi Anglong', state_id: 2 },
// Tripura (ID 22)
{ name: 'Dhalai', state_id: 22 }, { name: 'Gomati', state_id: 22 }, { name: 'Khowai', state_id: 22 }, { name: 'North Tripura', state_id: 22 }, { name: 'Sepahijala', state_id: 22 }, { name: 'South Tripura', state_id: 22 }, { name: 'Unakoti', state_id: 22 },
{ name: 'West Tripura', state_id: 22 }
];
module.exports = {
up: async (queryInterface, Sequelize) => {
try {
console.log('Starting migration...');
// 1. Create Zones and maintain a map for state lookup
const zoneMap = new Map(); // Name -> UUID
const zoneInserts = ZONES_DATA.map(zone => {
const id = uuidv4();
zoneMap.set(zone.name, id);
return {
id: id,
zoneCode: zone.code,
zoneName: zone.name,
description: zone.description,
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
};
});
// Insert Zones
if (zoneInserts.length > 0) {
await queryInterface.bulkInsert('zones', zoneInserts);
console.log(`Inserted ${zoneInserts.length} zones`);
}
// 2. Prepare States with Zone IDs
const stateMap = new Map(); // Legacy ID -> UUID (for city lookup)
const stateInserts = STATES_DATA.map(state => {
const id = uuidv4();
// Store both ID and ZoneID for lookup
stateMap.set(state.id, { id: id, zoneId: null }); // Will update zoneId below
// Find which Zone this state belongs to
let zoneId = null;
for (const z of ZONES_DATA) {
if (z.states.includes(state.name)) {
zoneId = zoneMap.get(z.name);
// Update the map with the found zoneId
stateMap.get(state.id).zoneId = zoneId;
break;
}
}
if (!zoneId) {
console.warn(`Warning: State ${state.name} not mapped to any Zone`);
}
return {
id: id,
stateName: state.name,
zoneId: zoneId,
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
// code and country_id not present in State.ts (based on my viewing)
// Let's re-verify State.ts.
// State.ts: stateName, zoneId, isActive. NO code, NO country_id.
};
});
// Insert States
if (stateInserts.length > 0) {
await queryInterface.bulkInsert('states', stateInserts);
console.log(`Inserted ${stateInserts.length} states`);
}
// 3. Prepare Districts (Cities) using State Map
const districtInserts = CITIES_DATA.map(city => {
const stateData = stateMap.get(city.state_id);
if (!stateData) {
console.warn(`Warning: City ${city.name} refers to unknown State ID ${city.state_id}`);
return null;
}
const { id: stateUuid, zoneId } = stateData;
return {
id: uuidv4(),
districtName: city.name,
districtName: city.name,
stateId: stateUuid,
zoneId: zoneId,
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
};
}).filter(d => d !== null);
// Insert Districts
// Bulk insert in chunks to avoid packet size issues if list is huge (it is ~380 items which is fine, but good practice)
if (districtInserts.length > 0) {
await queryInterface.bulkInsert('districts', districtInserts);
console.log(`Inserted ${districtInserts.length} districts`);
}
console.log('Migration completed successfully.');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
},
down: async (queryInterface, Sequelize) => {
// Delete in reverse order of foreign key dependency
// Delete in reverse order of foreign key dependency
await queryInterface.bulkDelete('districts', null, {});
await queryInterface.bulkDelete('states', null, {});
await queryInterface.bulkDelete('zones', null, {});
}
};

View File

@ -1,111 +0,0 @@
const { query } = require('../config/database');
/**
* Log audit trail for all important actions
*/
const logAudit = async ({ userId, action, entityType, entityId, oldValue = null, newValue = null, ipAddress = null, userAgent = null }) => {
try {
await query(
`INSERT INTO audit_logs
(user_id, action, entity_type, entity_id, old_value, new_value, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
userId,
action,
entityType,
entityId,
oldValue ? JSON.stringify(oldValue) : null,
newValue ? JSON.stringify(newValue) : null,
ipAddress,
userAgent
]
);
console.log(`Audit logged: ${action} by user ${userId}`);
} catch (error) {
console.error('Error logging audit:', error);
// Don't throw error - audit logging should not break the main flow
}
};
/**
* Get audit logs for an entity
*/
const getAuditLogs = async (entityType, entityId) => {
try {
const result = await query(
`SELECT al.*, u.full_name as user_name, u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.entity_type = $1 AND al.entity_id = $2
ORDER BY al.created_at DESC`,
[entityType, entityId]
);
return result.rows;
} catch (error) {
console.error('Error fetching audit logs:', error);
return [];
}
};
/**
* Get all audit logs with filters
*/
const getAllAuditLogs = async ({ userId, action, entityType, startDate, endDate, limit = 100 }) => {
try {
let queryText = `
SELECT al.*, u.full_name as user_name, u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE 1=1
`;
const params = [];
let paramCount = 1;
if (userId) {
queryText += ` AND al.user_id = $${paramCount}`;
params.push(userId);
paramCount++;
}
if (action) {
queryText += ` AND al.action = $${paramCount}`;
params.push(action);
paramCount++;
}
if (entityType) {
queryText += ` AND al.entity_type = $${paramCount}`;
params.push(entityType);
paramCount++;
}
if (startDate) {
queryText += ` AND al.created_at >= $${paramCount}`;
params.push(startDate);
paramCount++;
}
if (endDate) {
queryText += ` AND al.created_at <= $${paramCount}`;
params.push(endDate);
paramCount++;
}
queryText += ` ORDER BY al.created_at DESC LIMIT $${paramCount}`;
params.push(limit);
const result = await query(queryText, params);
return result.rows;
} catch (error) {
console.error('Error fetching all audit logs:', error);
return [];
}
};
module.exports = {
logAudit,
getAuditLogs,
getAllAuditLogs
};

View File

@ -9,9 +9,9 @@ export const generateToken = (user: any): string => {
const payload: TokenPayload = { const payload: TokenPayload = {
userId: user.id, userId: user.id,
email: user.email, email: user.email,
role: user.role, role: user.roleCode,
region: user.region, region: user.regionId,
zone: user.zone zone: user.zoneId
}; };
return jwt.sign(payload, JWT_SECRET, { return jwt.sign(payload, JWT_SECRET, {

View File

@ -157,6 +157,7 @@ export const AUDIT_ACTIONS = {
APPROVED: 'APPROVED', APPROVED: 'APPROVED',
REJECTED: 'REJECTED', REJECTED: 'REJECTED',
DELETED: 'DELETED', DELETED: 'DELETED',
LOGIN: 'LOGIN',
STAGE_CHANGED: 'STAGE_CHANGED', STAGE_CHANGED: 'STAGE_CHANGED',
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED', DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
WORKNOTE_ADDED: 'WORKNOTE_ADDED' WORKNOTE_ADDED: 'WORKNOTE_ADDED'

View File

@ -0,0 +1,25 @@
import 'dotenv/config';
export interface EmailConfig {
host: string;
port: number;
secure: boolean;
auth: {
user: string | undefined;
pass: string | undefined;
};
from: string;
}
const config: EmailConfig = {
host: process.env.EMAIL_HOST || 'smtp.gmail.com',
port: parseInt(process.env.EMAIL_PORT || '587'),
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
from: process.env.EMAIL_FROM || 'Royal Enfield <noreply@royalenfield.com>'
};
export default config;

View File

@ -0,0 +1,50 @@
/**
* Granular Permission Constants
* Categorized by Action, View/Access, and Application Stage
*/
export const PERMISSION_CATEGORIES = {
ACTION: 'ACTION',
VIEW: 'VIEW',
STAGE: 'STAGE'
} as const;
export const PERMISSIONS = {
// Action Permissions
ACTION_APPROVE: 'action:approve',
ACTION_REJECT: 'action:reject',
ACTION_UPLOAD_DOCS: 'action:upload_docs',
ACTION_REQUEST_CHANGES: 'action:request_changes',
ACTION_FORWARD: 'action:forward',
ACTION_REASSIGN: 'action:reassign',
ACTION_SCHEDULE_INTERVIEW: 'action:schedule_interview',
ACTION_ADD_COMMENTS: 'action:add_comments',
ACTION_RANK_APPLICANTS: 'action:rank_applicants',
ACTION_FINAL_APPROVAL: 'action:final_approval',
// View/Access Permissions
VIEW_DETAILS: 'view:view_details',
VIEW_FINANCIAL: 'view:view_financial',
VIEW_DISCUSSIONS: 'view:view_discussions',
VIEW_PROGRESS: 'view:view_progress',
VIEW_AUDIT: 'view:view_audit',
VIEW_DOCUMENTS: 'view:view_documents',
VIEW_PERSONAL: 'view:view_personal',
VIEW_BUSINESS: 'view:view_business',
VIEW_REPORTS: 'view:view_reports',
VIEW_HISTORY: 'view:view_history',
// Application Stage Access
STAGE_INITIAL_REVIEW: 'stage:initial_review',
STAGE_FIELD_VERIFICATION: 'stage:field_verification',
STAGE_LEVEL1_INTERVIEW: 'stage:level1_interview',
STAGE_LEVEL2_INTERVIEW: 'stage:level2_interview',
STAGE_RANKING: 'stage:ranking',
STAGE_LEGAL_REVIEW: 'stage:legal_review',
STAGE_FINANCIAL_REVIEW: 'stage:financial_review',
STAGE_FINAL_APPROVAL: 'stage:final_approval',
STAGE_PAYMENT: 'stage:payment',
STAGE_ONBOARDING: 'stage:onboarding'
} as const;
export type PermissionCode = typeof PERMISSIONS[keyof typeof PERMISSIONS];

View File

@ -2,6 +2,7 @@ import { Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import logger from '../utils/logger.js'; import logger from '../utils/logger.js';
import { JWT_SECRET } from '../config/auth.js';
import { AuthenticatedRequest, AuthRequest } from '../../types/express.types.js'; import { AuthenticatedRequest, AuthRequest } from '../../types/express.types.js';
export const authenticate = async (req: AuthRequest, res: Response, next: NextFunction) => { export const authenticate = async (req: AuthRequest, res: Response, next: NextFunction) => {
@ -19,11 +20,10 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
const token = authHeader.replace('Bearer ', ''); const token = authHeader.replace('Bearer ', '');
// Verify token // Verify token
const jwtSecret = process.env.JWT_SECRET || 'your-default-secret'; const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
const decoded = jwt.verify(token, jwtSecret) as { id: string };
// Find user // Find user
const user = await db.User.findByPk(decoded.id, { const user = await db.User.findByPk(decoded.userId, {
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
}); });
@ -79,10 +79,9 @@ export const optionalAuth = async (req: AuthRequest, res: Response, next: NextFu
} }
const token = authHeader.replace('Bearer ', ''); const token = authHeader.replace('Bearer ', '');
const jwtSecret = process.env.JWT_SECRET || 'your-default-secret'; const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
const decoded = jwt.verify(token, jwtSecret) as { id: string };
const user = await db.User.findByPk(decoded.id, { const user = await db.User.findByPk(decoded.userId, {
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
}); });

View File

@ -20,14 +20,14 @@ export const checkRole = (allowedRoles: string[]) => {
} }
// Check if user role is in allowed roles // Check if user role is in allowed roles
if (!allowedRoles.includes(req.user.role)) { if (!allowedRoles.includes(req.user.roleCode)) {
logger.warn(`Access denied for user ${req.user.email} (${req.user.role}) to route ${req.path}`); logger.warn(`Access denied for user ${req.user.email} (${req.user.roleCode}) to route ${req.path}`);
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: 'Access denied. Insufficient permissions.', message: 'Access denied. Insufficient permissions.',
requiredRoles: allowedRoles, requiredRoles: allowedRoles,
yourRole: req.user.role yourRole: req.user.roleCode
}); });
} }

View File

@ -3,6 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface AreaAttributes { export interface AreaAttributes {
id: string; id: string;
regionId: string; regionId: string;
stateId: string;
zoneId: string;
districtId: string; districtId: string;
areaCode: string; areaCode: string;
areaName: string; areaName: string;
@ -28,6 +30,22 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
stateId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'states',
key: 'id'
}
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
districtId: { districtId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: false,
@ -67,6 +85,14 @@ export default (sequelize: Sequelize) => {
foreignKey: 'regionId', foreignKey: 'regionId',
as: 'region' as: 'region'
}); });
Area.belongsTo(models.State, {
foreignKey: 'stateId',
as: 'state'
});
Area.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
Area.belongsTo(models.District, { Area.belongsTo(models.District, {
foreignKey: 'districtId', foreignKey: 'districtId',
as: 'district' as: 'district'

View File

@ -3,6 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DistrictAttributes { export interface DistrictAttributes {
id: string; id: string;
stateId: string; stateId: string;
zoneId: string;
regionId: string;
districtName: string; districtName: string;
isActive: boolean; isActive: boolean;
} }
@ -24,6 +26,22 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
districtName: { districtName: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
@ -34,8 +52,7 @@ export default (sequelize: Sequelize) => {
} }
}, { }, {
tableName: 'districts', tableName: 'districts',
timestamps: true, timestamps: true
updatedAt: false
}); });
(District as any).associate = (models: any) => { (District as any).associate = (models: any) => {
@ -43,6 +60,14 @@ export default (sequelize: Sequelize) => {
foreignKey: 'stateId', foreignKey: 'stateId',
as: 'state' as: 'state'
}); });
District.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
District.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
District.hasMany(models.Area, { District.hasMany(models.Area, {
foreignKey: 'districtId', foreignKey: 'districtId',
as: 'areas' as: 'areas'

View File

@ -4,6 +4,7 @@ export interface PermissionAttributes {
id: string; id: string;
permissionCode: string; permissionCode: string;
permissionName: string; permissionName: string;
permissionCategory: string;
module: string; module: string;
permissionType: string; permissionType: string;
action: string; action: string;
@ -28,6 +29,11 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
}, },
permissionCategory: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'GENERAL'
},
module: { module: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
@ -55,6 +61,12 @@ export default (sequelize: Sequelize) => {
foreignKey: 'permissionId', foreignKey: 'permissionId',
as: 'rolePermissions' as: 'rolePermissions'
}); });
Permission.belongsToMany(models.Role, {
through: models.RolePermission,
foreignKey: 'permissionId',
otherKey: 'roleId',
as: 'roles'
});
}; };
return Permission; return Permission;

View File

@ -45,12 +45,23 @@ export default (sequelize: Sequelize) => {
}); });
(Role as any).associate = (models: any) => { (Role as any).associate = (models: any) => {
Role.hasMany(models.User, {
foreignKey: 'roleCode',
sourceKey: 'roleCode',
as: 'users'
});
Role.hasMany(models.UserRole, { Role.hasMany(models.UserRole, {
foreignKey: 'roleId', foreignKey: 'roleId',
as: 'userRoles' as: 'userRoles'
}); });
Role.hasMany(models.RolePermission, { Role.hasMany(models.RolePermission, {
foreignKey: 'roleId', foreignKey: 'roleId',
as: 'rolePermissions'
});
Role.belongsToMany(models.Permission, {
through: models.RolePermission,
foreignKey: 'roleId',
otherKey: 'permissionId',
as: 'permissions' as: 'permissions'
}); });
}; };

View File

@ -4,11 +4,6 @@ export interface RolePermissionAttributes {
id: string; id: string;
roleId: string; roleId: string;
permissionId: string; permissionId: string;
canView: boolean;
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
canApprove: boolean;
} }
export interface RolePermissionInstance extends Model<RolePermissionAttributes>, RolePermissionAttributes { } export interface RolePermissionInstance extends Model<RolePermissionAttributes>, RolePermissionAttributes { }
@ -35,26 +30,6 @@ export default (sequelize: Sequelize) => {
model: 'permissions', model: 'permissions',
key: 'id' key: 'id'
} }
},
canView: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
canCreate: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
canEdit: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
canDelete: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
canApprove: {
type: DataTypes.BOOLEAN,
defaultValue: false
} }
}, { }, {
tableName: 'role_permissions', tableName: 'role_permissions',

View File

@ -35,8 +35,7 @@ export default (sequelize: Sequelize) => {
} }
}, { }, {
tableName: 'states', tableName: 'states',
timestamps: true, timestamps: true
updatedAt: false
}); });
(State as any).associate = (models: any) => { (State as any).associate = (models: any) => {

View File

@ -145,6 +145,11 @@ export default (sequelize: Sequelize) => {
}); });
(User as any).associate = (models: any) => { (User as any).associate = (models: any) => {
User.belongsTo(models.Role, {
foreignKey: 'roleCode',
targetKey: 'roleCode',
as: 'role'
});
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' }); User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' }); User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });

View File

@ -9,14 +9,28 @@ import { AuthRequest } from '../../types/express.types.js';
export const getRoles = async (req: Request, res: Response) => { export const getRoles = async (req: Request, res: Response) => {
try { try {
const roles = await Role.findAll({ const roles = await Role.findAll({
include: [{ include: [
{
model: Permission, model: Permission,
as: 'permissions', as: 'permissions',
through: { attributes: ['canCreate', 'canRead', 'canUpdate', 'canDelete', 'canApprove'] } through: { attributes: [] }
}], },
{
model: User,
as: 'users',
attributes: ['id']
}
],
order: [['roleName', 'ASC']] order: [['roleName', 'ASC']]
}); });
res.json({ success: true, data: roles });
// Map to include userCount
const result = roles.map((r: any) => ({
...r.toJSON(),
userCount: r.users?.length || 0
}));
res.json({ success: true, data: result });
} catch (error) { } catch (error) {
console.error('Get roles error:', error); console.error('Get roles error:', error);
res.status(500).json({ success: false, message: 'Error fetching roles' }); res.status(500).json({ success: false, message: 'Error fetching roles' });
@ -25,22 +39,21 @@ export const getRoles = async (req: Request, res: Response) => {
export const createRole = async (req: AuthRequest, res: Response) => { export const createRole = async (req: AuthRequest, res: Response) => {
try { try {
const { roleCode, roleName, description, permissions } = req.body; // permissions: [{ permissionId, actions: { canCreate... } }] const { roleCode, roleName, description, permissionIds } = req.body; // permissionIds: string[]
const role = await Role.create({ roleCode, roleName, description }); const role = await Role.create({ roleCode, roleName, description });
if (permissions && permissions.length > 0) { if (permissionIds && permissionIds.length > 0) {
for (const p of permissions) { for (const pid of permissionIds) {
await RolePermission.create({ await RolePermission.create({
roleId: role.id, roleId: role.id,
permissionId: p.permissionId, permissionId: pid
...p.actions
}); });
} }
} }
await AuditLog.create({ await AuditLog.create({
userId: req.user?.id, // Optional chaining as user might be undefined if auth middleware fails or not strict userId: req.user?.id,
action: AUDIT_ACTIONS.CREATED, action: AUDIT_ACTIONS.CREATED,
entityType: 'role', entityType: 'role',
entityId: role.id, entityId: role.id,
@ -57,21 +70,20 @@ export const createRole = async (req: AuthRequest, res: Response) => {
export const updateRole = async (req: AuthRequest, res: Response) => { export const updateRole = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { roleName, description, permissions, isActive } = req.body; const { roleName, description, permissionIds, isActive } = req.body;
const role = await Role.findByPk(id); const role = await Role.findByPk(id);
if (!role) return res.status(404).json({ success: false, message: 'Role not found' }); if (!role) return res.status(404).json({ success: false, message: 'Role not found' });
await role.update({ roleName, description, isActive }); await role.update({ roleName, description, isActive });
if (permissions) { if (permissionIds) {
// Simplistic: Remove all and re-add (or smart update). for MVP redo all is fine or use bulkCreate with updateOnDuplicate // Remove existing permissions and re-add new ones
await RolePermission.destroy({ where: { roleId: id } }); await RolePermission.destroy({ where: { roleId: id } });
for (const p of permissions) { for (const pid of permissionIds) {
await RolePermission.create({ await RolePermission.create({
roleId: id, roleId: id,
permissionId: p.permissionId, permissionId: pid
...p.actions
}); });
} }
} }
@ -109,7 +121,22 @@ export const getAllUsers = async (req: Request, res: Response) => {
try { try {
const users = await User.findAll({ const users = await User.findAll({
attributes: { exclude: ['password'] }, attributes: { exclude: ['password'] },
include: ['roleDetails', 'zoneDetails', 'regionDetails', 'areaDetails'], // Assuming associations are named like this or similar include: [
{
model: Role,
as: 'role',
include: [
{
model: Permission,
as: 'permissions',
through: { attributes: [] }
}
]
},
{ model: db.Zone, as: 'zone' },
{ model: db.Region, as: 'region' },
{ model: db.Area, as: 'area' }
],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
res.json({ success: true, data: users }); res.json({ success: true, data: users });
@ -144,6 +171,52 @@ export const updateUserStatus = async (req: AuthRequest, res: Response) => {
} }
}; };
export const updateUser = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const {
fullName, email, roleCode, status, isActive, employeeId,
mobileNumber, department, designation,
zoneId, regionId, stateId, districtId, areaId
} = req.body;
const user = await User.findByPk(id);
if (!user) return res.status(404).json({ success: false, message: 'User not found' });
const oldData = user.toJSON();
await user.update({
fullName: fullName || user.fullName,
email: email || user.email,
roleCode: roleCode || user.roleCode,
status: status || user.status,
isActive: isActive !== undefined ? isActive : user.isActive,
employeeId: employeeId || user.employeeId,
mobileNumber: mobileNumber || user.mobileNumber,
department: department || user.department,
designation: designation || user.designation,
zoneId: zoneId !== undefined ? zoneId : user.zoneId,
regionId: regionId !== undefined ? regionId : user.regionId,
stateId: stateId !== undefined ? stateId : user.stateId,
districtId: districtId !== undefined ? districtId : user.districtId,
areaId: areaId !== undefined ? areaId : user.areaId
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'user',
entityId: id,
oldData,
newData: req.body
});
res.json({ success: true, message: 'User updated successfully', data: user });
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({ success: false, message: 'Error updating user' });
}
};
// --- Dealer Codes --- // --- Dealer Codes ---
export const generateDealerCode = async (req: AuthRequest, res: Response) => { export const generateDealerCode = async (req: AuthRequest, res: Response) => {

View File

@ -20,6 +20,7 @@ router.get('/permissions', adminController.getPermissions);
// Users (Admin View) // Users (Admin View)
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);
// Dealer Codes // Dealer Codes
router.post('/dealer-codes/generate', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.generateDealerCode); router.post('/dealer-codes/generate', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.generateDealerCode);

View File

@ -35,11 +35,11 @@ export const register = async (req: Request, res: Response) => {
const user = await User.create({ const user = await User.create({
email, email,
password: hashedPassword, password: hashedPassword,
name: fullName, fullName,
role, roleCode: role,
phone, mobileNumber: phone,
region, regionId: region,
zone, zoneId: zone,
status: 'active' status: 'active'
}); });
@ -114,7 +114,7 @@ export const login = async (req: Request, res: Response) => {
// Log audit // Log audit
await AuditLog.create({ await AuditLog.create({
userId: user.id, userId: user.id,
action: 'user_login' as any, action: AUDIT_ACTIONS.LOGIN as any,
entityType: 'user', entityType: 'user',
entityId: user.id entityId: user.id
}); });
@ -125,10 +125,10 @@ export const login = async (req: Request, res: Response) => {
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,
fullName: user.name, fullName: user.fullName,
role: user.role, role: user.roleCode,
region: user.region, region: user.regionId,
zone: user.zone zone: user.zoneId
} }
}); });
} catch (error) { } catch (error) {
@ -148,7 +148,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
} }
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'name', 'role', 'region', 'zone', 'phone', 'createdAt'] attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt']
}); });
if (!user) { if (!user) {
@ -163,12 +163,12 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,
fullName: user.name, fullName: user.fullName,
role: user.role, role: user.roleCode,
region: user.region, region: user.regionId,
zone: user.zone, zone: user.zoneId,
phone: user.phone, phone: user.mobileNumber,
createdAt: user.createdAt createdAt: (user as any).createdAt
} }
}); });
} catch (error) { } catch (error) {
@ -198,8 +198,8 @@ export const updateProfile = async (req: AuthRequest, res: Response) => {
} }
await user.update({ await user.update({
name: fullName || user.name, fullName: fullName || user.fullName,
phone: phone || user.phone mobileNumber: phone || user.mobileNumber
}); });
// Log audit // Log audit

View File

@ -8,6 +8,7 @@ router.post('/register', authController.register);
router.post('/login', authController.login); router.post('/login', authController.login);
// Protected routes // Protected routes
router.get('/me', authenticate as any, authController.getProfile);
router.get('/profile', authenticate as any, authController.getProfile); router.get('/profile', authenticate as any, authController.getProfile);
router.put('/profile', authenticate as any, authController.updateProfile); router.put('/profile', authenticate as any, authController.updateProfile);
router.post('/change-password', authenticate as any, authController.changePassword); router.post('/change-password', authenticate as any, authController.changePassword);

View File

@ -1,15 +1,15 @@
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 } = db; const { Region, Zone, State, District, Area, User } = 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({
order: [['name', 'ASC']] order: [['regionName', 'ASC']]
}); });
res.json({ success: true, regions }); res.json({ success: true, data: regions });
} catch (error) { } catch (error) {
console.error('Get regions error:', error); console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' }); res.status(500).json({ success: false, message: 'Error fetching regions' });
@ -24,7 +24,7 @@ export const createRegion = async (req: Request, res: Response) => {
return res.status(400).json({ success: false, message: 'Region name is required' }); return res.status(400).json({ success: false, message: 'Region name is required' });
} }
const region = await Region.create({ name: regionName }); const region = await Region.create({ regionName });
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) {
@ -44,7 +44,7 @@ export const updateRegion = async (req: Request, res: Response) => {
} }
await region.update({ await region.update({
name: regionName || region.name, regionName: regionName || (region as any).regionName,
updatedAt: new Date() updatedAt: new Date()
}); });
@ -67,15 +67,27 @@ export const getZones = async (req: Request, res: Response) => {
const zones = await Zone.findAll({ const zones = await Zone.findAll({
where, where,
include: [{ include: [
{
model: Region, model: Region,
as: 'region', as: 'regions',
attributes: ['name'] attributes: ['regionName']
}], },
order: [['name', 'ASC']] {
model: User,
as: 'zonalBusinessHead',
attributes: ['fullName', 'email', 'mobileNumber']
},
{
model: State,
as: 'states',
attributes: ['stateName']
}
],
order: [['zoneName', 'ASC']]
}); });
res.json({ success: true, zones }); res.json({ success: true, data: zones });
} catch (error) { } catch (error) {
console.error('Get zones error:', error); console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' }); res.status(500).json({ success: false, message: 'Error fetching zones' });
@ -91,8 +103,8 @@ export const createZone = async (req: Request, res: Response) => {
} }
const zone = await Zone.create({ const zone = await Zone.create({
regionId, regionId, // Wait, Zone Model doesn't have regionId. It's the other way around?
name: zoneName zoneName
}); });
res.status(201).json({ success: true, message: 'Zone created successfully', data: zone }); res.status(201).json({ success: true, message: 'Zone created successfully', data: zone });
@ -113,7 +125,7 @@ export const updateZone = async (req: Request, res: Response) => {
} }
await zone.update({ await zone.update({
name: zoneName || zone.name, zoneName: zoneName || (zone as any).zoneName,
updatedAt: new Date() updatedAt: new Date()
}); });
@ -133,7 +145,7 @@ export const getStates = async (req: Request, res: Response) => {
const states = await State.findAll({ const states = await State.findAll({
where, where,
include: [{ model: Zone, as: 'zone', attributes: ['name'] }], include: [{ model: Zone, as: 'zone', attributes: ['zoneName'] }],
order: [['stateName', 'ASC']] order: [['stateName', 'ASC']]
}); });
res.json({ success: true, states }); res.json({ success: true, states });

View File

@ -5,8 +5,9 @@ export interface AuthenticatedRequest extends Request {
user?: { user?: {
id: string; id: string;
email: string; email: string;
name: string; fullName: string;
role: typeof ROLES[keyof typeof ROLES]; roleCode: string;
role?: typeof ROLES[keyof typeof ROLES];
}; };
} }

View File

@ -33,7 +33,6 @@
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist", "dist",
"tests", "tests"
"scripts"
] ]
} }