flow created upto outlet creation

This commit is contained in:
laxmanhalaki 2026-03-20 19:31:38 +05:30
parent c6946eae4e
commit 6dc506077f
13 changed files with 375 additions and 29 deletions

View File

@ -0,0 +1,63 @@
import { Sequelize } from 'sequelize';
import config from '../src/common/config/database.js';
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: console.log
}
);
async function migrate() {
try {
await sequelize.authenticate();
console.log('Connected to database.');
const queryInterface = sequelize.getQueryInterface();
// Check if users table exists
const tables = await queryInterface.showAllTables();
if (!tables.includes('users')) {
console.log('Users table does not exist. Skipping rename.');
return;
}
// Rename fullName to name
const columns = await queryInterface.describeTable('users');
if (columns.fullName && !columns.name) {
console.log('Renaming fullName to name...');
await queryInterface.renameColumn('users', 'fullName', 'name');
} else if (columns.fullName && columns.name) {
console.log('Both fullName and name exist. Manual intervention needed.');
} else {
console.log('fullName not found or name already exists.');
}
// Rename mobileNumber to phone
if (columns.mobileNumber && !columns.phone) {
console.log('Renaming mobileNumber to phone...');
await queryInterface.renameColumn('users', 'mobileNumber', 'phone');
} else if (columns.mobileNumber && columns.phone) {
console.log('Both mobileNumber and phone exist. Manual intervention needed.');
} else {
console.log('mobileNumber not found or phone already exists.');
}
console.log('Migration successful.');
} catch (error) {
console.error('Migration failed:', error);
} finally {
await sequelize.close();
}
}
migrate();

View File

@ -289,6 +289,7 @@ export default (sequelize: Sequelize) => {
scope: { requestType: 'application' } scope: { requestType: 'application' }
}); });
Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' }); Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' });
Application.hasOne(models.Dealer, { foreignKey: 'applicationId', as: 'dealer' });
}; };
return Application; return Application;

View File

@ -83,6 +83,7 @@ export default (sequelize: Sequelize) => {
Dealer.hasMany(models.Document, { foreignKey: 'dealerId', as: 'documents' }); Dealer.hasMany(models.Document, { foreignKey: 'dealerId', as: 'documents' });
Dealer.hasMany(models.Resignation, { foreignKey: 'dealerId', as: 'resignations' }); Dealer.hasMany(models.Resignation, { foreignKey: 'dealerId', as: 'resignations' });
Dealer.hasMany(models.TerminationRequest, { foreignKey: 'dealerId', as: 'terminationRequests' }); Dealer.hasMany(models.TerminationRequest, { foreignKey: 'dealerId', as: 'terminationRequests' });
Dealer.hasOne(models.User, { foreignKey: 'dealerId', as: 'user' });
}; };
return Dealer; return Dealer;

View File

@ -160,6 +160,7 @@ export default (sequelize: Sequelize) => {
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' }); User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' });
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
}; };
return User; return User;

View File

@ -309,7 +309,7 @@ export const addParticipant = async (req: AuthRequest, res: Response) => {
user.email, user.email,
user.fullName, user.fullName,
application.applicationId || application.id, application.applicationId || application.id,
application.name, // The dealer's name application.applicantName, // The dealer's name
participantType || 'participant' participantType || 'participant'
); );
} }

View File

@ -1,6 +1,10 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { Dealer, DealerCode, Application, User, AuditLog } = db; const {
Dealer, DealerCode, Application, User, AuditLog, Outlet, Region, Zone,
Resignation, RelocationRequest, ConstitutionalChange
} = db;
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AUDIT_ACTIONS } from '../../common/config/constants.js';
@ -27,27 +31,160 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
const application = await Application.findByPk(applicationId); const application = await Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Mark Code as Used // Find existing dealer or auto-detect dealer code
if (dealerCodeId) { let targetDealerCodeId = dealerCodeId;
await DealerCode.update({ isUsed: true, usedAt: new Date(), usedByApplicationId: applicationId }, { where: { id: dealerCodeId } }); if (!targetDealerCodeId) {
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } });
if (dealerCodeRecord) {
targetDealerCodeId = dealerCodeRecord.id;
}
} }
const dealer = await Dealer.create({ const existingDealer = await Dealer.findOne({ where: { applicationId } });
applicationId, let dealer = existingDealer;
dealerCodeId,
dealerName: application.applicantName, // Or Trade Name if (existingDealer) {
status: 'Active', // Update existing dealer if it's missing the code
onboardedAt: new Date() if (!existingDealer.dealerCodeId && targetDealerCodeId) {
}); await existingDealer.update({ dealerCodeId: targetDealerCodeId });
// Mark Code as Used
await DealerCode.update({
isUsed: true,
usedAt: new Date(),
usedByApplicationId: applicationId
}, { where: { id: targetDealerCodeId } });
}
} else {
// Mark Code as Used
if (targetDealerCodeId) {
await DealerCode.update({
isUsed: true,
usedAt: new Date(),
usedByApplicationId: applicationId
}, { where: { id: targetDealerCodeId } });
}
// Create Dealer Profile
dealer = await Dealer.create({
applicationId,
dealerCodeId: targetDealerCodeId,
legalName: application.applicantName,
businessName: application.applicantName,
constitutionType: application.businessType,
status: 'Active',
onboardedAt: new Date()
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'dealer',
entityId: dealer.id
});
}
// --- Create or Update User Account for the Dealer ---
let user = await User.findOne({ where: { email: application.email } });
if (user) {
// Update existing user to Dealer role and link dealerId
await user.update({
roleCode: 'Dealer',
dealerId: dealer.id,
status: 'active',
isActive: true
});
console.log(`[Dealer Onboarding] Updated existing user ${user.email} to Dealer role.`);
} else {
// Create new User account
const hashedPassword = await bcrypt.hash('Dealer@123', 10); // Default password
user = await User.create({
fullName: application.applicantName,
email: application.email,
password: hashedPassword,
roleCode: 'Dealer',
dealerId: dealer.id,
mobileNumber: application.phone,
status: 'active',
isActive: true,
isExternal: true, // Dealers are external users
zoneId: application.zoneId,
regionId: application.regionId
});
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
}
await AuditLog.create({ await AuditLog.create({
userId: req.user?.id, userId: req.user?.id,
action: AUDIT_ACTIONS.CREATED, action: AUDIT_ACTIONS.CREATED,
entityType: 'dealer', entityType: 'user',
entityId: dealer.id entityId: user.id,
newData: { roleCode: 'Dealer', dealerId: dealer.id }
}); });
res.status(201).json({ success: true, message: 'Dealer profile created', data: dealer }); // --- Create Primary Outlet for the Dealer ---
// Check if outlet already exists
let outlet = await Outlet.findOne({ where: { dealerId: user.id } });
if (!outlet) {
let regionName = 'Central';
let zoneName = 'National';
if (application.regionId) {
const reg = await Region.findByPk(application.regionId);
if (reg) regionName = reg.regionName;
}
if (application.zoneId) {
const zon = await Zone.findByPk(application.zoneId);
if (zon) zoneName = zon.zoneName;
}
// Map region name to valid ENUM values if necessary
const validRegions = ['East', 'West', 'North', 'South', 'Central'];
if (!validRegions.includes(regionName)) {
regionName = 'Central'; // Fallback
}
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } });
const outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`;
outlet = await Outlet.create({
code: outletCode,
name: `${application.applicantName} - ${application.city || 'Primary'}`,
type: application.businessType === 'Studio' ? 'Studio' : 'Dealership',
address: application.address || application.businessAddress || 'Address Pending',
city: application.city || 'City Pending',
state: application.state || 'State Pending',
pincode: application.pincode || '000000',
status: 'Active',
establishedDate: new Date(),
dealerId: user.id,
region: regionName as any,
zone: zoneName
});
console.log(`[Dealer Onboarding] Created primary outlet ${outlet.code} for dealer ${user.email}.`);
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'outlet',
entityId: outlet.id,
newData: { code: outlet.code, name: outlet.name }
});
}
res.status(201).json({
success: true,
message: 'Dealer profile, user account, and primary outlet created successfully',
data: {
dealer,
user: { email: user.email, role: user.roleCode },
outlet: { code: outlet.code, name: outlet.name }
}
});
} catch (error) { } catch (error) {
console.error('Create dealer error:', error); console.error('Create dealer error:', error);
res.status(500).json({ success: false, message: 'Error creating dealer' }); res.status(500).json({ success: false, message: 'Error creating dealer' });
@ -69,3 +206,68 @@ export const updateDealer = async (req: AuthRequest, res: Response) => {
res.status(500).json({ success: false, message: 'Error updating dealer' }); res.status(500).json({ success: false, message: 'Error updating dealer' });
} }
}; };
export const getDealerDashboard = async (req: AuthRequest, res: Response) => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ success: false, message: 'Unauthorized' });
// 1. Fetch User and linked Dealer Profile
const user = await User.findByPk(userId, {
include: [{
association: 'dealerProfile',
include: [{ association: 'dealerCode' }]
}]
});
// 2. Fetch Active Outlets
const outlets = await Outlet.findAll({
where: { dealerId: userId },
order: [['createdAt', 'DESC']]
});
// 3. Fetch Request Counts & Recent History
const [resignations, relocations, constitutional] = await Promise.all([
Resignation.findAll({ where: { dealerId: userId }, order: [['createdAt', 'DESC']] }),
RelocationRequest.findAll({ where: { dealerId: userId }, order: [['createdAt', 'DESC']] }),
ConstitutionalChange.findAll({ where: { dealerId: userId }, order: [['createdAt', 'DESC']] })
]);
// Combine and sort recent requests
const allRequests = [
...resignations.map((r: any) => ({ id: r.resignationId, type: 'Resignation', title: r.reason, status: r.status, date: r.submittedOn || (r as any).createdAt, color: 'bg-red-100 text-red-700 border-red-300' })),
...relocations.map((r: any) => ({ id: r.requestId, type: 'Relocation', title: `Relocation to ${r.newCity}`, status: r.status, date: (r as any).createdAt, color: 'bg-amber-100 text-amber-700 border-amber-300' })),
...constitutional.map((r: any) => ({ id: r.requestId, type: 'Constitutional Change', title: r.changeType, status: r.status, date: (r as any).createdAt, color: 'bg-blue-100 text-blue-700 border-blue-300' }))
].sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5);
res.json({
success: true,
data: {
profile: {
name: user?.fullName,
email: user?.email,
dealerCode: (user as any)?.dealerProfile?.dealerCode?.code || 'N/A',
businessName: (user as any)?.dealerProfile?.businessName || 'N/A'
},
outlets: outlets.map((o: any) => ({
id: o.id,
name: o.name,
code: o.code,
type: o.type,
status: o.status,
location: `${o.city}, ${o.state}`
})),
stats: {
constitutional: constitutional.length,
relocation: relocations.length,
resignation: resignations.length,
total: constitutional.length + relocations.length + resignations.length
},
recentRequests: allRequests
}
});
} catch (error) {
console.error('Get dealer dashboard error:', error);
res.status(500).json({ success: false, message: 'Error fetching dashboard data' });
}
};

View File

@ -5,6 +5,7 @@ import { authenticate } from '../../common/middleware/auth.js';
router.use(authenticate as any); router.use(authenticate as any);
router.get('/dashboard', dealerController.getDealerDashboard);
router.get('/', dealerController.getDealers); router.get('/', dealerController.getDealers);
router.post('/', dealerController.createDealer); router.post('/', dealerController.createDealer);
router.put('/:id', dealerController.updateDealer); router.put('/:id', dealerController.updateDealer);

View File

@ -18,7 +18,7 @@ export const getOutlets = async (req: AuthRequest, res: Response) => {
{ {
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name', 'email'] attributes: ['fullName', 'email']
}, },
{ {
model: Resignation, model: Resignation,
@ -54,7 +54,7 @@ export const getOutletById = async (req: AuthRequest, res: Response) => {
include: [{ include: [{
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name', 'email', 'phone'] attributes: ['fullName', 'email', 'mobileNumber']
}] }]
}); });
@ -197,7 +197,7 @@ export const getOutletByCode = async (req: AuthRequest, res: Response) => {
include: [{ include: [{
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name', 'email', 'phone'] attributes: ['fullName', 'email', 'mobileNumber']
}] }]
}); });

View File

@ -190,12 +190,9 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
} }
] ]
}, },
{ { model: db.RequestParticipant, as: 'participants', include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] },
model: db.RequestParticipant, { model: db.DealerCode, as: 'dealerCode' },
as: 'participants', { model: db.Dealer, as: 'dealer' }
include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }]
},
{ model: db.DealerCode, as: 'dealerCode' }
] ]
}); });

View File

@ -158,7 +158,7 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex
{ {
model: db.User, model: db.User,
as: 'dealer', as: 'dealer',
attributes: ['id', 'name', 'email', 'phone'] attributes: ['id', 'fullName', 'email', 'mobileNumber']
} }
], ],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
@ -199,7 +199,7 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
{ {
model: db.User, model: db.User,
as: 'dealer', as: 'dealer',
attributes: ['id', 'name', 'email', 'phone'] attributes: ['id', 'fullName', 'email', 'mobileNumber']
} }
] ]
}, },

View File

@ -41,7 +41,7 @@ export const getFnFSettlements = async (req: Request, res: Response) => {
include: [{ include: [{
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name', 'id'] attributes: ['fullName', 'id']
}] }]
}, },
{ {

View File

@ -0,0 +1,69 @@
import db from '../database/models/index.js';
const { User, Dealer, Outlet, Application, DealerCode } = db;
async function fixMissingOutlets() {
try {
console.log('--- Starting Missing Outlet Fix ---');
// 1. Find all users with role 'Dealer'
const dealerUsers = await User.findAll({
where: { roleCode: 'Dealer' }
});
console.log(`Found ${dealerUsers.length} dealer users.`);
for (const user of dealerUsers) {
// 2. Check if this dealer already has an outlet
const existingOutlet = await Outlet.findOne({ where: { dealerId: user.id } });
if (existingOutlet) {
console.log(`Dealer ${user.email} already has outlet: ${existingOutlet.code}. Skipping.`);
continue;
}
console.log(`Dealer ${user.email} is missing an outlet. Attempting to create one...`);
// 3. Find the dealer profile to get the application
const dealerProfile = await Dealer.findOne({ where: { id: user.dealerId } });
if (!dealerProfile) {
console.warn(`Could not find dealer profile for user ${user.email}. Skipping.`);
continue;
}
const application = await Application.findByPk(dealerProfile.applicationId);
if (!application) {
console.warn(`Could not find application for dealer ${user.email}. Skipping.`);
continue;
}
// 4. Create the outlet (same logic as in dealer.controller.ts)
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } });
const outletCode = `OUT-${dealerCodeRecord ? (dealerCodeRecord.dealerCode || dealerCodeRecord.code) : Date.now().toString().slice(-6)}`;
const outlet = await Outlet.create({
code: outletCode,
name: `${application.applicantName} - ${application.city || 'Primary'}`,
type: application.businessType === 'Studio' ? 'Studio' : 'Dealership',
address: application.address || application.businessAddress || 'Address Pending',
city: application.city || 'City Pending',
state: application.state || 'State Pending',
pincode: application.pincode || '000000',
status: 'Active',
establishedDate: new Date(),
dealerId: user.id,
region: 'Central', // Default fallback
zone: 'National'
});
console.log(`Successfully created outlet ${outlet.code} for dealer ${user.email}.`);
}
console.log('--- Fix Completed ---');
process.exit(0);
} catch (error) {
console.error('Error fixing missing outlets:', error);
process.exit(1);
}
}
fixMissingOutlets();

View File

@ -128,19 +128,30 @@ app.use('/api/questionnaire', questionnaireRoutes);
app.use('/api/prospective-login', prospectiveLoginRoutes); app.use('/api/prospective-login', prospectiveLoginRoutes);
app.use('/api/termination', terminationRoutes); app.use('/api/termination', terminationRoutes);
// Backward Compatibility Aliases // Backward Compatibility & Frontend Mapping Aliases
app.use('/api/applications', onboardingRoutes); app.use('/api/applications', onboardingRoutes);
app.use('/api/resignations', resignationRoutes); app.use('/api/resignations', resignationRoutes);
app.use('/api/resignation', resignationRoutes); // Singular alias
app.use('/api/constitutional', (req: Request, res: Response, next: NextFunction) => { app.use('/api/constitutional', (req: Request, res: Response, next: NextFunction) => {
// Map /api/constitutional to /api/self-service/constitutional // Map /api/constitutional to /api/self-service/constitutional
req.url = '/constitutional' + req.url; req.url = '/constitutional' + req.url;
next(); next();
}, selfServiceRoutes); }, selfServiceRoutes);
app.use('/api/constitutional-change', (req: Request, res: Response, next: NextFunction) => {
// Alias for constitutional-change
req.url = '/constitutional' + req.url;
next();
}, selfServiceRoutes);
app.use('/api/relocations', (req: Request, res: Response, next: NextFunction) => { app.use('/api/relocations', (req: Request, res: Response, next: NextFunction) => {
// Map /api/relocations to /api/self-service/relocation // Map /api/relocations to /api/self-service/relocation
req.url = '/relocation' + req.url; req.url = '/relocation' + req.url;
next(); next();
}, selfServiceRoutes); }, selfServiceRoutes);
app.use('/api/relocation', (req: Request, res: Response, next: NextFunction) => {
// Alias for relocation
req.url = '/relocation' + req.url;
next();
}, selfServiceRoutes);
app.use('/api/outlets', outletRoutes); app.use('/api/outlets', outletRoutes);
app.use('/api/finance', settlementRoutes); app.use('/api/finance', settlementRoutes);
app.use('/api/worknotes', collaborationRoutes); app.use('/api/worknotes', collaborationRoutes);
@ -167,7 +178,7 @@ const startServer = async () => {
// Sync database (in development only) // Sync database (in development only)
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
await db.sequelize.sync({ alter: true }); await db.sequelize.sync({ alter: false });
logger.info('Database models synchronized'); logger.info('Database models synchronized');
} }