started progress bar end to end flowe with mockup at backend for external dependecies

This commit is contained in:
laxmanhalaki 2026-03-19 20:21:36 +05:30
parent c43f86253b
commit c6946eae4e
14 changed files with 415 additions and 34 deletions

18
scripts/check_column.ts Normal file
View File

@ -0,0 +1,18 @@
import db from '../src/database/models/index.js';
async function checkColumn() {
try {
const [results]: any = await db.sequelize.query(`
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_name = 'request_participants' AND column_name = 'participantType'
`);
console.log('Column definition:', results[0]);
} catch (error: any) {
console.error('Error fetching column:', error.message);
} finally {
process.exit(0);
}
}
checkColumn();

19
scripts/check_enum.ts Normal file
View File

@ -0,0 +1,19 @@
import db from '../src/database/models/index.js';
async function checkEnum() {
try {
const [results]: any = await db.sequelize.query(`
SELECT enumlabel
FROM pg_enum
JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
WHERE typname = 'enum_request_participants_participantType'
`);
console.log('Current enum values:', results.map((r: any) => r.enumlabel).join(', '));
} catch (error: any) {
console.error('Error fetching enum:', error.message);
} finally {
process.exit(0);
}
}
checkEnum();

26
scripts/test_enum_cast.ts Normal file
View File

@ -0,0 +1,26 @@
import db from '../src/database/models/index.js';
async function testInsert() {
try {
// Attempt insert without checking existing records
// If it fails with "invalid input value", the enum is truly not updated.
// If it fails with "foreign key", the enum was VALID but the data was wrong.
await db.sequelize.query(`
DO $$
BEGIN
-- This will fail if 'architecture' is invalid for the enum
PERFORM 'architecture'::"enum_request_participants_participantType";
RAISE NOTICE '✅ Enum check passed!';
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION '❌ Enum check failed: %', SQLERRM;
END $$;
`);
console.log('✅ PL/pgSQL Enum check passed!');
} catch (error: any) {
console.error(error.message);
} finally {
process.exit(0);
}
}
testInsert();

22
scripts/test_insert.ts Normal file
View File

@ -0,0 +1,22 @@
import db from '../src/database/models/index.js';
async function testInsert() {
try {
const testId = '00000000-0000-0000-0000-000000000000';
await db.sequelize.query(`
INSERT INTO request_participants ("id", "requestId", "requestType", "userId", "participantType", "joinedMethod", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), '${testId}', 'test', '9950ee60-ddf6-4091-a1e6-e7161e6d8bb6', 'architecture', 'manual', now(), now())
`);
console.log('✅ Manual insert successful!');
// Clean up
await db.sequelize.query(`DELETE FROM request_participants WHERE "requestId" = '${testId}'`);
console.log('✅ Clean up successful!');
} catch (error: any) {
console.error('❌ Manual insert failed:', error.message);
} finally {
process.exit(0);
}
}
testInsert();

View File

@ -0,0 +1,46 @@
import db from '../src/database/models/index.js';
const { sequelize } = db;
async function updateDealerCodesTable() {
console.log('🔄 Checking and updating dealer_codes table schema...');
try {
// Add applicationId
await sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='applicationId') THEN
ALTER TABLE dealer_codes ADD COLUMN "applicationId" UUID REFERENCES applications(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='salesCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "salesCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='serviceCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "serviceCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gmaCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "gmaCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gearCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "gearCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='sapMasterId') THEN
ALTER TABLE dealer_codes ADD COLUMN "sapMasterId" VARCHAR(255);
END IF;
END $$;
`);
console.log('✅ dealer_codes table schema updated successfully.');
process.exit(0);
} catch (error) {
console.error('❌ Error updating dealer_codes table:', error);
process.exit(1);
}
}
updateDealerCodesTable();

33
scripts/update_enum.ts Normal file
View File

@ -0,0 +1,33 @@
import { APPLICATION_STATUS } from '../src/common/config/constants.js';
import db from '../src/database/models/index.js';
async function updateEnum() {
try {
console.log('🔄 Syncing all APPLICATION_STATUS values with DB Enum...');
const statuses = Object.values(APPLICATION_STATUS);
for (const status of statuses) {
try {
// Posgres doesn't support IF NOT EXISTS for ADD VALUE in 9.5 and below
// so we do it one by one and ignore "already exists" errors.
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE '${status}'`);
console.log(`✅ Added: ${status}`);
} catch (e: any) {
if (e.message.includes('already exists')) {
// console.log(` Already exists: ${status}`);
} else {
console.error(`❌ Error adding ${status}:`, e.message);
}
}
}
console.log('\n✅ Database Enum successfully synchronized with constants.');
} catch (error: any) {
console.error('❌ Critical failure during sync:', error.message);
} finally {
process.exit(0);
}
}
updateEnum();

View File

@ -0,0 +1,26 @@
import db from '../src/database/models/index.js';
async function updateParticipantEnum() {
try {
console.log('🔄 Adding "architecture" to participantType enum...');
try {
await db.sequelize.query(`ALTER TYPE "enum_request_participants_participantType" ADD VALUE 'architecture'`);
console.log(`✅ Added: architecture`);
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log(` Already exists: architecture`);
} else {
console.error(`❌ Error adding architecture:`, e.message);
}
}
console.log('\n✅ Database Enum successfully updated.');
} catch (error: any) {
console.error('❌ Critical failure:', error.message);
} finally {
process.exit(0);
}
}
updateParticipantEnum();

View File

@ -63,6 +63,7 @@ export const APPLICATION_STATUS = {
LEVEL_3_APPROVED: 'Level 3 Approved',
FDD_VERIFICATION: 'FDD Verification',
PAYMENT_PENDING: 'Payment Pending',
LOI_IN_PROGRESS: 'LOI In Progress',
LOI_ISSUED: 'LOI Issued',
DEALER_CODE_GENERATION: 'Dealer Code Generation',
ARCHITECTURE_TEAM_ASSIGNED: 'Architecture Team Assigned',
@ -304,6 +305,18 @@ export const DOCUMENT_TYPES = {
BOARD_RESOLUTION: 'Board Resolution',
PROPERTY_DOCUMENTS: 'Property Documents',
BANK_STATEMENT: 'Bank Statement',
NODAL_AGREEMENT: 'Nodal Agreement',
CANCELLED_CHECK: 'Cancelled Check',
FIRM_REGISTRATION: 'Firm Registration',
RENTAL_AGREEMENT: 'Rental Agreement',
VIRTUAL_CODE: 'Virtual Code Confirmation',
DOMAIN_ID: 'Domain ID Setup',
MSD_CONFIG: 'MSD Configuration',
LOI_ACK: 'LOI Acknowledgement',
ARCHITECTURE_ASSIGNMENT: 'Architecture Assignment Document',
ARCHITECTURE_BLUEPRINT: 'Architecture Blueprint',
SITE_PLAN: 'Site Plan',
ARCHITECTURE_COMPLETION: 'Architecture Completion Certificate',
OTHER: 'Other'
} as const;

View File

@ -37,6 +37,9 @@ export interface ApplicationAttributes {
zoneId: string | null;
regionId: string | null;
areaId: string | null;
architectureAssignedDate: Date | null;
architectureDocumentDate: Date | null;
architectureCompletionDate: Date | null;
score: number;
documents: any[];
timeline: any[];
@ -223,6 +226,18 @@ export default (sequelize: Sequelize) => {
key: 'id'
}
},
architectureAssignedDate: {
type: DataTypes.DATE,
allowNull: true
},
architectureDocumentDate: {
type: DataTypes.DATE,
allowNull: true
},
architectureCompletionDate: {
type: DataTypes.DATE,
allowNull: true
},
documents: {
type: DataTypes.JSON,
defaultValue: []
@ -273,6 +288,7 @@ export default (sequelize: Sequelize) => {
as: 'participants',
scope: { requestType: 'application' }
});
Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' });
};
return Application;

View File

@ -3,6 +3,12 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DealerCodeAttribute {
id: string;
dealerCode: string;
applicationId: string | null;
salesCode: string | null;
serviceCode: string | null;
gmaCode: string | null;
gearCode: string | null;
sapMasterId: string | null;
status: string;
generatedAt: Date;
generatedBy: string | null;
@ -22,6 +28,34 @@ export default (sequelize: Sequelize) => {
unique: true,
allowNull: false
},
applicationId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'applications',
key: 'id'
}
},
salesCode: {
type: DataTypes.STRING,
allowNull: true
},
serviceCode: {
type: DataTypes.STRING,
allowNull: true
},
gmaCode: {
type: DataTypes.STRING,
allowNull: true
},
gearCode: {
type: DataTypes.STRING,
allowNull: true
},
sapMasterId: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
@ -47,6 +81,7 @@ export default (sequelize: Sequelize) => {
(DealerCode as any).associate = (models: any) => {
DealerCode.belongsTo(models.User, { foreignKey: 'generatedBy', as: 'generator' });
DealerCode.hasOne(models.Dealer, { foreignKey: 'dealerCodeId', as: 'dealer' });
DealerCode.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
};
return DealerCode;

View File

@ -36,7 +36,7 @@ export default (sequelize: Sequelize) => {
}
},
participantType: {
type: DataTypes.ENUM('owner', 'assignee', 'reviewer', 'contributor', 'observer'),
type: DataTypes.ENUM('owner', 'assignee', 'reviewer', 'contributor', 'observer', 'architecture'),
defaultValue: 'contributor'
},
joinedMethod: {

View File

@ -163,15 +163,26 @@ export const createUser = async (req: AuthRequest, res: Response) => {
});
}
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
// Check if user already exists (Email)
const existingEmail = await User.findOne({ where: { email } });
if (existingEmail) {
return res.status(400).json({
success: false,
message: 'User with this email already exists'
});
}
// Check if user already exists (Employee ID)
if (employeeId) {
const existingEmpId = await User.findOne({ where: { employeeId } });
if (existingEmpId) {
return res.status(400).json({
success: false,
message: `User with Employee ID ${employeeId} already exists`
});
}
}
// Hash default password
const hashedPassword = await bcrypt.hash('Admin@123', 10);
@ -203,8 +214,16 @@ export const createUser = async (req: AuthRequest, res: Response) => {
});
res.status(201).json({ success: true, message: 'User created successfully', data: user });
} catch (error) {
} catch (error: any) {
console.error('Create user error:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
const field = error.errors[0]?.path || 'field';
const value = error.errors[0]?.value || '';
return res.status(400).json({
success: false,
message: `${field} "${value}" already exists. Please use a unique value.`
});
}
res.status(500).json({ success: false, message: 'Error creating user' });
}
};
@ -282,8 +301,16 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
});
res.json({ success: true, message: 'User updated successfully', data: user });
} catch (error) {
} catch (error: any) {
console.error('Update user error:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
const field = error.errors[0]?.path || 'field';
const value = error.errors[0]?.value || '';
return res.status(400).json({
success: false,
message: `${field} "${value}" already exists. Please use a unique value.`
});
}
res.status(500).json({ success: false, message: 'Error updating user' });
}
};

View File

@ -38,18 +38,20 @@ export const createChecklist = async (req: AuthRequest, res: Response) => {
});
if (created) {
// Define Default Mandatory Items per SRS
// Define Default Mandatory Items per SRS/Frontend
const defaultItems = [
{ itemType: 'Architecture', description: 'Brand Signage & Facade as per guidelines' },
{ itemType: 'Architecture', description: 'Interior Fit-out & Furniture' },
{ itemType: 'Sales', description: 'Display Vehicles (All models) available' },
{ itemType: 'Sales', description: 'Test Ride Vehicles registered' },
{ itemType: 'Training', description: 'Sales Staff (DSE) Training Completed' },
{ itemType: 'Training', description: 'Service Technician (Pro-Meck) Training' },
{ itemType: 'IT', description: 'DMS (Dealer Management System) configured' },
{ itemType: 'IT', description: 'High-speed internet & IT Hardware ready' },
{ itemType: 'Service', description: 'Special Tools & Equipment installed' },
{ itemType: 'Finance', description: 'Bank Account mapped in SAP' }
{ itemType: 'Sales', description: 'Sales Standards' },
{ itemType: 'Service', description: 'Service & Spares' },
{ itemType: 'IT', description: 'DMS infra' },
{ itemType: 'Training', description: 'Manpower Training' },
{ itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
{ itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
{ itemType: 'Finance', description: 'Inventory Funding' },
{ itemType: 'IT', description: 'Virtual code availability' },
{ itemType: 'Finance', description: 'Vendor payments' },
{ itemType: 'Marketing', description: 'Details for website submission' },
{ itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
{ itemType: 'IT', description: 'Auto ordering' }
];
const itemsData = defaultItems.map(item => ({
@ -75,15 +77,23 @@ export const updateItem = async (req: AuthRequest, res: Response) => {
const { checklistId } = req.params;
const { itemType, description, isCompliant, remarks, proofDocumentId } = req.body;
const item = await EorChecklistItem.create({
checklistId,
itemType,
description,
isCompliant,
remarks,
proofDocumentId
let item = await EorChecklistItem.findOne({
where: { checklistId, description }
});
if (item) {
await item.update({ isCompliant, remarks, proofDocumentId, itemType });
} else {
item = await EorChecklistItem.create({
checklistId,
itemType,
description,
isCompliant,
remarks,
proofDocumentId
});
}
res.status(201).json({ success: true, message: 'Item added/updated', data: item });
} catch (error) {
console.error('Update EOR item error:', error);

View File

@ -194,7 +194,8 @@ 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' }
]
});
@ -297,6 +298,54 @@ export const uploadDocuments = async (req: any, res: Response) => {
status: 'active'
});
// Update architecture document date if relevant
if (['Architecture Blueprint', 'Site Plan'].includes(documentType)) {
await application.update({ architectureDocumentDate: new Date() });
}
// Handle EOR Checklist Automatic Linking & Compliance
const eorItems = [
{ itemType: 'Sales', description: 'Sales Standards' },
{ itemType: 'Service', description: 'Service & Spares' },
{ itemType: 'IT', description: 'DMS infra' },
{ itemType: 'Training', description: 'Manpower Training' },
{ itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
{ itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
{ itemType: 'Finance', description: 'Inventory Funding' },
{ itemType: 'IT', description: 'Virtual code availability' },
{ itemType: 'Finance', description: 'Vendor payments' },
{ itemType: 'Marketing', description: 'Details for website submission' },
{ itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
{ itemType: 'IT', description: 'Auto ordering' }
];
const eorDescriptions = eorItems.map(i => i.description);
if (eorDescriptions.includes(documentType)) {
// Find or Create Checklist
const [checklist, created] = await db.EorChecklist.findOrCreate({
where: { applicationId: application.id },
defaults: { status: 'In Progress' }
});
// If newly created or no items exist, seed them
const existingItemCount = await db.EorChecklistItem.count({ where: { checklistId: checklist.id } });
if (created || existingItemCount === 0) {
const itemsData = eorItems.map(item => ({
...item,
checklistId: checklist.id,
isCompliant: false
}));
await db.EorChecklistItem.bulkCreate(itemsData);
}
// Update the matching item - Link only, don't auto-verify (requested by user)
await db.EorChecklistItem.update(
{ proofDocumentId: newDoc.id, isCompliant: false },
{ where: { checklistId: checklist.id, description: documentType } }
);
}
res.status(201).json({
success: true,
message: 'Document uploaded successfully',
@ -424,14 +473,21 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { userId, remarks } = req.body;
const { userId, assignedTo, remarks } = req.body;
const targetUserId = userId || assignedTo;
if (!targetUserId) {
return res.status(400).json({ success: false, message: 'Architecture team member (userId) is required' });
}
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update({
architectureAssignedTo: userId,
architectureStatus: 'Assigned',
architectureAssignedTo: targetUserId,
architectureStatus: 'IN_PROGRESS',
architectureAssignedDate: new Date(),
overallStatus: 'Architecture Team Assigned',
updatedAt: new Date()
});
@ -440,7 +496,7 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) =>
where: {
requestId: application.id,
requestType: 'application',
userId,
userId: targetUserId,
participantType: 'architecture'
},
defaults: { joinedMethod: 'auto' }
@ -451,7 +507,7 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) =>
action: AUDIT_ACTIONS.UPDATED,
entityType: 'application',
entityId: application.id,
newData: { architectureAssignedTo: userId, remarks }
newData: { architectureAssignedTo: targetUserId, remarks }
});
res.json({ success: true, message: 'Architecture team assigned successfully' });
@ -469,10 +525,20 @@ export const updateArchitectureStatus = async (req: AuthRequest, res: Response)
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update({
const updateData: any = {
architectureStatus: status,
updatedAt: new Date()
});
};
// Sync overall status if architecture is completed
if (status === 'COMPLETED') {
updateData.overallStatus = 'Architecture Team Completion';
updateData.architectureCompletionDate = new Date();
} else if (status === 'IN_PROGRESS') {
updateData.overallStatus = 'Architecture Team Assigned';
}
await application.update(updateData);
await AuditLog.create({
userId: req.user?.id,
@ -503,20 +569,44 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
// Save Dealer Codes
await db.DealerCode.create({
dealerCode: sapData.salesCode, // Use sales code as primary dealer code
applicationId: id,
salesCode: sapData.salesCode,
serviceCode: sapData.serviceCode,
gmaCode: sapData.gmaCode,
gearCode: sapData.gearCode,
sapMasterId: sapData.sapMasterId,
status: 'Active'
status: 'Active',
generatedBy: req.user?.id
});
const previousStatus = application.overallStatus;
// Update application status to reflect codes are generated
// We STAY in Dealer Code Generation until architecture is assigned
await application.update({
overallStatus: 'Architecture Team Assigned',
overallStatus: 'Dealer Code Generation',
progressPercentage: 80
});
// Log Status History
await db.ApplicationStatusHistory.create({
applicationId: application.id,
previousStatus,
newStatus: 'Dealer Code Generation',
changedBy: req.user?.id,
reason: 'SAP Dealer Codes Generated'
});
// Audit Log
await db.AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.DEALER_CODE_GENERATED,
entityType: 'application',
entityId: application.id,
newData: { dealerCode: sapData.salesCode }
});
res.json({
success: true,
message: 'SAP Dealer Codes generated successfully (Mock)',