From 6dc506077fcdbc0d91afd06554ba9adac9c57d4d Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 20 Mar 2026 19:31:38 +0530 Subject: [PATCH] flow created upto outlet creation --- scripts/migrate_user_columns.ts | 63 +++++ src/database/models/Application.ts | 1 + src/database/models/Dealer.ts | 1 + src/database/models/User.ts | 1 + .../collaboration/collaboration.controller.ts | 2 +- src/modules/dealer/dealer.controller.ts | 230 ++++++++++++++++-- src/modules/dealer/dealer.routes.ts | 1 + src/modules/master/outlet.controller.ts | 6 +- .../onboarding/onboarding.controller.ts | 9 +- .../self-service/resignation.controller.ts | 4 +- .../settlement/settlement.controller.ts | 2 +- src/scripts/fix_missing_outlets.ts | 69 ++++++ src/server.ts | 15 +- 13 files changed, 375 insertions(+), 29 deletions(-) create mode 100644 scripts/migrate_user_columns.ts create mode 100644 src/scripts/fix_missing_outlets.ts diff --git a/scripts/migrate_user_columns.ts b/scripts/migrate_user_columns.ts new file mode 100644 index 0000000..ec5d714 --- /dev/null +++ b/scripts/migrate_user_columns.ts @@ -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(); diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index b17a8f5..6714af5 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -289,6 +289,7 @@ export default (sequelize: Sequelize) => { scope: { requestType: 'application' } }); Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' }); + Application.hasOne(models.Dealer, { foreignKey: 'applicationId', as: 'dealer' }); }; return Application; diff --git a/src/database/models/Dealer.ts b/src/database/models/Dealer.ts index a1b5219..40992fc 100644 --- a/src/database/models/Dealer.ts +++ b/src/database/models/Dealer.ts @@ -83,6 +83,7 @@ export default (sequelize: Sequelize) => { Dealer.hasMany(models.Document, { foreignKey: 'dealerId', as: 'documents' }); Dealer.hasMany(models.Resignation, { foreignKey: 'dealerId', as: 'resignations' }); Dealer.hasMany(models.TerminationRequest, { foreignKey: 'dealerId', as: 'terminationRequests' }); + Dealer.hasOne(models.User, { foreignKey: 'dealerId', as: 'user' }); }; return Dealer; diff --git a/src/database/models/User.ts b/src/database/models/User.ts index 133623c..900313e 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -160,6 +160,7 @@ export default (sequelize: Sequelize) => { User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' }); + User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' }); }; return User; diff --git a/src/modules/collaboration/collaboration.controller.ts b/src/modules/collaboration/collaboration.controller.ts index 6930d9d..231335d 100644 --- a/src/modules/collaboration/collaboration.controller.ts +++ b/src/modules/collaboration/collaboration.controller.ts @@ -309,7 +309,7 @@ export const addParticipant = async (req: AuthRequest, res: Response) => { user.email, user.fullName, application.applicationId || application.id, - application.name, // The dealer's name + application.applicantName, // The dealer's name participantType || 'participant' ); } diff --git a/src/modules/dealer/dealer.controller.ts b/src/modules/dealer/dealer.controller.ts index 62f433b..309c57c 100644 --- a/src/modules/dealer/dealer.controller.ts +++ b/src/modules/dealer/dealer.controller.ts @@ -1,6 +1,10 @@ import { Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; 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 { 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); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); - // Mark Code as Used - if (dealerCodeId) { - await DealerCode.update({ isUsed: true, usedAt: new Date(), usedByApplicationId: applicationId }, { where: { id: dealerCodeId } }); + // Find existing dealer or auto-detect dealer code + let targetDealerCodeId = dealerCodeId; + if (!targetDealerCodeId) { + const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } }); + if (dealerCodeRecord) { + targetDealerCodeId = dealerCodeRecord.id; + } } - const dealer = await Dealer.create({ - applicationId, - dealerCodeId, - dealerName: application.applicantName, // Or Trade Name - status: 'Active', - onboardedAt: new Date() - }); + const existingDealer = await Dealer.findOne({ where: { applicationId } }); + let dealer = existingDealer; + + if (existingDealer) { + // Update existing dealer if it's missing the code + 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({ userId: req.user?.id, action: AUDIT_ACTIONS.CREATED, - entityType: 'dealer', - entityId: dealer.id + entityType: 'user', + 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) { console.error('Create dealer error:', error); 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' }); } }; + +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' }); + } +}; diff --git a/src/modules/dealer/dealer.routes.ts b/src/modules/dealer/dealer.routes.ts index 0194f0b..ff3914a 100644 --- a/src/modules/dealer/dealer.routes.ts +++ b/src/modules/dealer/dealer.routes.ts @@ -5,6 +5,7 @@ import { authenticate } from '../../common/middleware/auth.js'; router.use(authenticate as any); +router.get('/dashboard', dealerController.getDealerDashboard); router.get('/', dealerController.getDealers); router.post('/', dealerController.createDealer); router.put('/:id', dealerController.updateDealer); diff --git a/src/modules/master/outlet.controller.ts b/src/modules/master/outlet.controller.ts index f0d17e1..cd2e217 100644 --- a/src/modules/master/outlet.controller.ts +++ b/src/modules/master/outlet.controller.ts @@ -18,7 +18,7 @@ export const getOutlets = async (req: AuthRequest, res: Response) => { { model: User, as: 'dealer', - attributes: ['name', 'email'] + attributes: ['fullName', 'email'] }, { model: Resignation, @@ -54,7 +54,7 @@ export const getOutletById = async (req: AuthRequest, res: Response) => { include: [{ model: User, as: 'dealer', - attributes: ['name', 'email', 'phone'] + attributes: ['fullName', 'email', 'mobileNumber'] }] }); @@ -197,7 +197,7 @@ export const getOutletByCode = async (req: AuthRequest, res: Response) => { include: [{ model: User, as: 'dealer', - attributes: ['name', 'email', 'phone'] + attributes: ['fullName', 'email', 'mobileNumber'] }] }); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 7383f20..92532c5 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -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.DealerCode, as: 'dealerCode' } + { model: db.RequestParticipant, as: 'participants', include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] }, + { model: db.DealerCode, as: 'dealerCode' }, + { model: db.Dealer, as: 'dealer' } ] }); diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 8732395..851c4be 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -158,7 +158,7 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex { model: db.User, as: 'dealer', - attributes: ['id', 'name', 'email', 'phone'] + attributes: ['id', 'fullName', 'email', 'mobileNumber'] } ], order: [['createdAt', 'DESC']], @@ -199,7 +199,7 @@ export const getResignationById = async (req: AuthRequest, res: Response, next: { model: db.User, as: 'dealer', - attributes: ['id', 'name', 'email', 'phone'] + attributes: ['id', 'fullName', 'email', 'mobileNumber'] } ] }, diff --git a/src/modules/settlement/settlement.controller.ts b/src/modules/settlement/settlement.controller.ts index 85c8560..a173985 100644 --- a/src/modules/settlement/settlement.controller.ts +++ b/src/modules/settlement/settlement.controller.ts @@ -41,7 +41,7 @@ export const getFnFSettlements = async (req: Request, res: Response) => { include: [{ model: User, as: 'dealer', - attributes: ['name', 'id'] + attributes: ['fullName', 'id'] }] }, { diff --git a/src/scripts/fix_missing_outlets.ts b/src/scripts/fix_missing_outlets.ts new file mode 100644 index 0000000..f415150 --- /dev/null +++ b/src/scripts/fix_missing_outlets.ts @@ -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(); diff --git a/src/server.ts b/src/server.ts index cd04ea6..dbece2a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -128,19 +128,30 @@ app.use('/api/questionnaire', questionnaireRoutes); app.use('/api/prospective-login', prospectiveLoginRoutes); app.use('/api/termination', terminationRoutes); -// Backward Compatibility Aliases +// Backward Compatibility & Frontend Mapping Aliases app.use('/api/applications', onboardingRoutes); app.use('/api/resignations', resignationRoutes); +app.use('/api/resignation', resignationRoutes); // Singular alias app.use('/api/constitutional', (req: Request, res: Response, next: NextFunction) => { // Map /api/constitutional to /api/self-service/constitutional req.url = '/constitutional' + req.url; next(); }, 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) => { // Map /api/relocations to /api/self-service/relocation req.url = '/relocation' + req.url; next(); }, 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/finance', settlementRoutes); app.use('/api/worknotes', collaborationRoutes); @@ -167,7 +178,7 @@ const startServer = async () => { // Sync database (in development only) if (process.env.NODE_ENV === 'development') { - await db.sequelize.sync({ alter: true }); + await db.sequelize.sync({ alter: false }); logger.info('Database models synchronized'); }