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:
parent
251a362717
commit
f54501793c
@ -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
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
@ -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>'
|
||||
};
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
@ -17,6 +17,7 @@ erDiagram
|
||||
string department
|
||||
string designation
|
||||
uuid role_code FK
|
||||
string role_code UK
|
||||
uuid zone_id FK
|
||||
uuid region_id FK
|
||||
uuid state_id FK
|
||||
@ -48,7 +49,7 @@ erDiagram
|
||||
string permission_code UK
|
||||
string permission_name
|
||||
string module
|
||||
string permission_type
|
||||
string permission_category
|
||||
string action
|
||||
string description
|
||||
timestamp created_at
|
||||
@ -58,11 +59,6 @@ erDiagram
|
||||
uuid role_permission_id PK
|
||||
uuid role_id FK
|
||||
uuid permission_id FK
|
||||
boolean can_view
|
||||
boolean can_create
|
||||
boolean can_edit
|
||||
boolean can_delete
|
||||
boolean can_approve
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
@ -115,6 +111,8 @@ erDiagram
|
||||
DISTRICTS {
|
||||
uuid district_id PK
|
||||
uuid state_id FK
|
||||
uuid zone_id FK
|
||||
uuid region_id FK
|
||||
string district_name
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
@ -122,7 +120,9 @@ erDiagram
|
||||
|
||||
AREAS {
|
||||
uuid area_id PK
|
||||
uuid zone_id FK
|
||||
uuid region_id FK
|
||||
uuid state_id FK
|
||||
uuid district_id FK
|
||||
string area_code UK
|
||||
string area_name
|
||||
@ -1067,15 +1067,19 @@ erDiagram
|
||||
%% RELATIONSHIPS
|
||||
%% ============================================
|
||||
USERS ||--o{ USER_ROLES : "has"
|
||||
ROLES ||--o{ USER_ROLES : "assigned_to"
|
||||
ROLES ||--o{ USERS : "assigned_to"
|
||||
ROLES ||--o{ ROLE_PERMISSIONS : "has"
|
||||
PERMISSIONS ||--o{ ROLE_PERMISSIONS : "granted_in"
|
||||
|
||||
ZONES ||--o{ STATES : "contains"
|
||||
STATES ||--o{ DISTRICTS : "contains"
|
||||
ZONES ||--o{ DISTRICTS : "contains"
|
||||
REGIONS ||--o{ DISTRICTS : "contains"
|
||||
ZONES ||--o{ REGIONS : "contains"
|
||||
STATES ||--o{ REGIONS : "contains"
|
||||
ZONES ||--o{ AREAS : "contains"
|
||||
REGIONS ||--o{ AREAS : "contains"
|
||||
STATES ||--o{ AREAS : "contains"
|
||||
DISTRICTS ||--o{ AREAS : "contains"
|
||||
|
||||
ZONES ||--o{ ZONE_MANAGERS : "managed_by"
|
||||
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
@ -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
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
104
models/Outlet.js
104
models/Outlet.js
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
@ -9,7 +9,8 @@
|
||||
"dev": "tsx watch src/server.ts",
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit",
|
||||
"migrate": "node dist/scripts/migrate.js",
|
||||
"migrate": "tsx scripts/migrate.ts",
|
||||
"seed": "tsx scripts/seed-geo.ts",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage",
|
||||
"clear-logs": "rm -rf logs/*.log"
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
27
scripts/seed-geo.ts
Normal 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
310
scripts/seed-permissions.ts
Normal 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
38
scripts/seed-roles.ts
Normal 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
52
scripts/seed-users.ts
Normal 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();
|
||||
160
scripts/seed.js
160
scripts/seed.js
@ -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();
|
||||
314
seeders/20240127-seed-geo-data.js
Normal file
314
seeders/20240127-seed-geo-data.js
Normal 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, {});
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -9,9 +9,9 @@ export const generateToken = (user: any): string => {
|
||||
const payload: TokenPayload = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
region: user.region,
|
||||
zone: user.zone
|
||||
role: user.roleCode,
|
||||
region: user.regionId,
|
||||
zone: user.zoneId
|
||||
};
|
||||
|
||||
return jwt.sign(payload, JWT_SECRET, {
|
||||
|
||||
@ -157,6 +157,7 @@ export const AUDIT_ACTIONS = {
|
||||
APPROVED: 'APPROVED',
|
||||
REJECTED: 'REJECTED',
|
||||
DELETED: 'DELETED',
|
||||
LOGIN: 'LOGIN',
|
||||
STAGE_CHANGED: 'STAGE_CHANGED',
|
||||
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
|
||||
WORKNOTE_ADDED: 'WORKNOTE_ADDED'
|
||||
|
||||
25
src/common/config/email.ts
Normal file
25
src/common/config/email.ts
Normal 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;
|
||||
50
src/common/config/permissions.ts
Normal file
50
src/common/config/permissions.ts
Normal 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];
|
||||
@ -2,6 +2,7 @@ import { Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import db from '../../database/models/index.js';
|
||||
import logger from '../utils/logger.js';
|
||||
import { JWT_SECRET } from '../config/auth.js';
|
||||
import { AuthenticatedRequest, AuthRequest } from '../../types/express.types.js';
|
||||
|
||||
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 ', '');
|
||||
|
||||
// Verify token
|
||||
const jwtSecret = process.env.JWT_SECRET || 'your-default-secret';
|
||||
const decoded = jwt.verify(token, jwtSecret) as { id: string };
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
|
||||
// Find user
|
||||
const user = await db.User.findByPk(decoded.id, {
|
||||
const user = await db.User.findByPk(decoded.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
@ -79,10 +79,9 @@ export const optionalAuth = async (req: AuthRequest, res: Response, next: NextFu
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const jwtSecret = process.env.JWT_SECRET || 'your-default-secret';
|
||||
const decoded = jwt.verify(token, jwtSecret) as { id: string };
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
|
||||
const user = await db.User.findByPk(decoded.id, {
|
||||
const user = await db.User.findByPk(decoded.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
|
||||
@ -20,14 +20,14 @@ export const checkRole = (allowedRoles: string[]) => {
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
if (!allowedRoles.includes(req.user.roleCode)) {
|
||||
logger.warn(`Access denied for user ${req.user.email} (${req.user.roleCode}) to route ${req.path}`);
|
||||
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Access denied. Insufficient permissions.',
|
||||
requiredRoles: allowedRoles,
|
||||
yourRole: req.user.role
|
||||
yourRole: req.user.roleCode
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||
export interface AreaAttributes {
|
||||
id: string;
|
||||
regionId: string;
|
||||
stateId: string;
|
||||
zoneId: string;
|
||||
districtId: string;
|
||||
areaCode: string;
|
||||
areaName: string;
|
||||
@ -28,6 +30,22 @@ export default (sequelize: Sequelize) => {
|
||||
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: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
@ -67,6 +85,14 @@ export default (sequelize: Sequelize) => {
|
||||
foreignKey: 'regionId',
|
||||
as: 'region'
|
||||
});
|
||||
Area.belongsTo(models.State, {
|
||||
foreignKey: 'stateId',
|
||||
as: 'state'
|
||||
});
|
||||
Area.belongsTo(models.Zone, {
|
||||
foreignKey: 'zoneId',
|
||||
as: 'zone'
|
||||
});
|
||||
Area.belongsTo(models.District, {
|
||||
foreignKey: 'districtId',
|
||||
as: 'district'
|
||||
|
||||
@ -3,6 +3,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||
export interface DistrictAttributes {
|
||||
id: string;
|
||||
stateId: string;
|
||||
zoneId: string;
|
||||
regionId: string;
|
||||
districtName: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@ -24,6 +26,22 @@ export default (sequelize: Sequelize) => {
|
||||
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: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
@ -34,8 +52,7 @@ export default (sequelize: Sequelize) => {
|
||||
}
|
||||
}, {
|
||||
tableName: 'districts',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
(District as any).associate = (models: any) => {
|
||||
@ -43,6 +60,14 @@ export default (sequelize: Sequelize) => {
|
||||
foreignKey: 'stateId',
|
||||
as: 'state'
|
||||
});
|
||||
District.belongsTo(models.Zone, {
|
||||
foreignKey: 'zoneId',
|
||||
as: 'zone'
|
||||
});
|
||||
District.belongsTo(models.Region, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'region'
|
||||
});
|
||||
District.hasMany(models.Area, {
|
||||
foreignKey: 'districtId',
|
||||
as: 'areas'
|
||||
|
||||
@ -4,6 +4,7 @@ export interface PermissionAttributes {
|
||||
id: string;
|
||||
permissionCode: string;
|
||||
permissionName: string;
|
||||
permissionCategory: string;
|
||||
module: string;
|
||||
permissionType: string;
|
||||
action: string;
|
||||
@ -28,6 +29,11 @@ export default (sequelize: Sequelize) => {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
permissionCategory: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'GENERAL'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
@ -55,6 +61,12 @@ export default (sequelize: Sequelize) => {
|
||||
foreignKey: 'permissionId',
|
||||
as: 'rolePermissions'
|
||||
});
|
||||
Permission.belongsToMany(models.Role, {
|
||||
through: models.RolePermission,
|
||||
foreignKey: 'permissionId',
|
||||
otherKey: 'roleId',
|
||||
as: 'roles'
|
||||
});
|
||||
};
|
||||
|
||||
return Permission;
|
||||
|
||||
@ -45,12 +45,23 @@ export default (sequelize: Sequelize) => {
|
||||
});
|
||||
|
||||
(Role as any).associate = (models: any) => {
|
||||
Role.hasMany(models.User, {
|
||||
foreignKey: 'roleCode',
|
||||
sourceKey: 'roleCode',
|
||||
as: 'users'
|
||||
});
|
||||
Role.hasMany(models.UserRole, {
|
||||
foreignKey: 'roleId',
|
||||
as: 'userRoles'
|
||||
});
|
||||
Role.hasMany(models.RolePermission, {
|
||||
foreignKey: 'roleId',
|
||||
as: 'rolePermissions'
|
||||
});
|
||||
Role.belongsToMany(models.Permission, {
|
||||
through: models.RolePermission,
|
||||
foreignKey: 'roleId',
|
||||
otherKey: 'permissionId',
|
||||
as: 'permissions'
|
||||
});
|
||||
};
|
||||
|
||||
@ -4,11 +4,6 @@ export interface RolePermissionAttributes {
|
||||
id: string;
|
||||
roleId: string;
|
||||
permissionId: string;
|
||||
canView: boolean;
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
canApprove: boolean;
|
||||
}
|
||||
|
||||
export interface RolePermissionInstance extends Model<RolePermissionAttributes>, RolePermissionAttributes { }
|
||||
@ -35,26 +30,6 @@ export default (sequelize: Sequelize) => {
|
||||
model: 'permissions',
|
||||
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',
|
||||
|
||||
@ -35,8 +35,7 @@ export default (sequelize: Sequelize) => {
|
||||
}
|
||||
}, {
|
||||
tableName: 'states',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
(State as any).associate = (models: any) => {
|
||||
|
||||
@ -145,6 +145,11 @@ export default (sequelize: Sequelize) => {
|
||||
});
|
||||
|
||||
(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: 'assignedBy', as: 'assignedRoles' });
|
||||
User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
||||
|
||||
@ -9,14 +9,28 @@ import { AuthRequest } from '../../types/express.types.js';
|
||||
export const getRoles = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
include: [{
|
||||
model: Permission,
|
||||
as: 'permissions',
|
||||
through: { attributes: ['canCreate', 'canRead', 'canUpdate', 'canDelete', 'canApprove'] }
|
||||
}],
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: 'permissions',
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'users',
|
||||
attributes: ['id']
|
||||
}
|
||||
],
|
||||
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) {
|
||||
console.error('Get roles error:', error);
|
||||
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) => {
|
||||
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 });
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
for (const p of permissions) {
|
||||
if (permissionIds && permissionIds.length > 0) {
|
||||
for (const pid of permissionIds) {
|
||||
await RolePermission.create({
|
||||
roleId: role.id,
|
||||
permissionId: p.permissionId,
|
||||
...p.actions
|
||||
permissionId: pid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
entityType: 'role',
|
||||
entityId: role.id,
|
||||
@ -57,21 +70,20 @@ export const createRole = async (req: AuthRequest, res: Response) => {
|
||||
export const updateRole = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { roleName, description, permissions, isActive } = req.body;
|
||||
const { roleName, description, permissionIds, isActive } = req.body;
|
||||
|
||||
const role = await Role.findByPk(id);
|
||||
if (!role) return res.status(404).json({ success: false, message: 'Role not found' });
|
||||
|
||||
await role.update({ roleName, description, isActive });
|
||||
|
||||
if (permissions) {
|
||||
// Simplistic: Remove all and re-add (or smart update). for MVP redo all is fine or use bulkCreate with updateOnDuplicate
|
||||
if (permissionIds) {
|
||||
// Remove existing permissions and re-add new ones
|
||||
await RolePermission.destroy({ where: { roleId: id } });
|
||||
for (const p of permissions) {
|
||||
for (const pid of permissionIds) {
|
||||
await RolePermission.create({
|
||||
roleId: id,
|
||||
permissionId: p.permissionId,
|
||||
...p.actions
|
||||
permissionId: pid
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -109,7 +121,22 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
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']]
|
||||
});
|
||||
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 ---
|
||||
|
||||
export const generateDealerCode = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
@ -20,6 +20,7 @@ router.get('/permissions', adminController.getPermissions);
|
||||
// Users (Admin View)
|
||||
router.get('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getAllUsers);
|
||||
router.patch('/users/:id/status', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUserStatus);
|
||||
router.put('/users/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateUser);
|
||||
|
||||
// Dealer Codes
|
||||
router.post('/dealer-codes/generate', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.generateDealerCode);
|
||||
|
||||
@ -35,11 +35,11 @@ export const register = async (req: Request, res: Response) => {
|
||||
const user = await User.create({
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name: fullName,
|
||||
role,
|
||||
phone,
|
||||
region,
|
||||
zone,
|
||||
fullName,
|
||||
roleCode: role,
|
||||
mobileNumber: phone,
|
||||
regionId: region,
|
||||
zoneId: zone,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
@ -114,7 +114,7 @@ export const login = async (req: Request, res: Response) => {
|
||||
// Log audit
|
||||
await AuditLog.create({
|
||||
userId: user.id,
|
||||
action: 'user_login' as any,
|
||||
action: AUDIT_ACTIONS.LOGIN as any,
|
||||
entityType: 'user',
|
||||
entityId: user.id
|
||||
});
|
||||
@ -125,10 +125,10 @@ export const login = async (req: Request, res: Response) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
fullName: user.name,
|
||||
role: user.role,
|
||||
region: user.region,
|
||||
zone: user.zone
|
||||
fullName: user.fullName,
|
||||
role: user.roleCode,
|
||||
region: user.regionId,
|
||||
zone: user.zoneId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -148,7 +148,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -163,12 +163,12 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
||||
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
|
||||
fullName: user.fullName,
|
||||
role: user.roleCode,
|
||||
region: user.regionId,
|
||||
zone: user.zoneId,
|
||||
phone: user.mobileNumber,
|
||||
createdAt: (user as any).createdAt
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -198,8 +198,8 @@ export const updateProfile = async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
|
||||
await user.update({
|
||||
name: fullName || user.name,
|
||||
phone: phone || user.phone
|
||||
fullName: fullName || user.fullName,
|
||||
mobileNumber: phone || user.mobileNumber
|
||||
});
|
||||
|
||||
// Log audit
|
||||
|
||||
@ -8,6 +8,7 @@ router.post('/register', authController.register);
|
||||
router.post('/login', authController.login);
|
||||
|
||||
// Protected routes
|
||||
router.get('/me', authenticate as any, authController.getProfile);
|
||||
router.get('/profile', authenticate as any, authController.getProfile);
|
||||
router.put('/profile', authenticate as any, authController.updateProfile);
|
||||
router.post('/change-password', authenticate as any, authController.changePassword);
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { Request, Response } from 'express';
|
||||
import db from '../../database/models/index.js';
|
||||
const { Region, Zone, State, District, Area } = db;
|
||||
const { Region, Zone, State, District, Area, User } = db;
|
||||
|
||||
// --- Regions ---
|
||||
export const getRegions = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const regions = await Region.findAll({
|
||||
order: [['name', 'ASC']]
|
||||
order: [['regionName', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({ success: true, regions });
|
||||
res.json({ success: true, data: regions });
|
||||
} catch (error) {
|
||||
console.error('Get regions error:', error);
|
||||
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' });
|
||||
}
|
||||
|
||||
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 });
|
||||
} catch (error) {
|
||||
@ -44,7 +44,7 @@ export const updateRegion = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
await region.update({
|
||||
name: regionName || region.name,
|
||||
regionName: regionName || (region as any).regionName,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
@ -67,15 +67,27 @@ export const getZones = async (req: Request, res: Response) => {
|
||||
|
||||
const zones = await Zone.findAll({
|
||||
where,
|
||||
include: [{
|
||||
model: Region,
|
||||
as: 'region',
|
||||
attributes: ['name']
|
||||
}],
|
||||
order: [['name', 'ASC']]
|
||||
include: [
|
||||
{
|
||||
model: Region,
|
||||
as: 'regions',
|
||||
attributes: ['regionName']
|
||||
},
|
||||
{
|
||||
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) {
|
||||
console.error('Get zones error:', error);
|
||||
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({
|
||||
regionId,
|
||||
name: zoneName
|
||||
regionId, // Wait, Zone Model doesn't have regionId. It's the other way around?
|
||||
zoneName
|
||||
});
|
||||
|
||||
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({
|
||||
name: zoneName || zone.name,
|
||||
zoneName: zoneName || (zone as any).zoneName,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
@ -133,7 +145,7 @@ export const getStates = async (req: Request, res: Response) => {
|
||||
|
||||
const states = await State.findAll({
|
||||
where,
|
||||
include: [{ model: Zone, as: 'zone', attributes: ['name'] }],
|
||||
include: [{ model: Zone, as: 'zone', attributes: ['zoneName'] }],
|
||||
order: [['stateName', 'ASC']]
|
||||
});
|
||||
res.json({ success: true, states });
|
||||
|
||||
@ -5,8 +5,9 @@ export interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: typeof ROLES[keyof typeof ROLES];
|
||||
fullName: string;
|
||||
roleCode: string;
|
||||
role?: typeof ROLES[keyof typeof ROLES];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,6 @@
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"tests",
|
||||
"scripts"
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user