Files
cim_summary/backend/src/services/csvExportService.ts
Jon c8c2783241 feat: Implement comprehensive CIM Review editing and admin features
- Add inline editing for CIM Review template with auto-save functionality
- Implement CSV export with comprehensive data formatting
- Add automated file naming (YYYYMMDD_CompanyName_CIM_Review.pdf/csv)
- Create admin role system for jpressnell@bluepointcapital.com
- Hide analytics/monitoring tabs from non-admin users
- Add email sharing functionality via mailto links
- Implement save status indicators and last saved timestamps
- Add backend endpoints for CIM Review save/load and CSV export
- Create admin service for role-based access control
- Update document viewer with save/export handlers
- Add proper error handling and user feedback

Backup: Live version preserved in backup-live-version-e0a37bf-clean branch
2025-08-14 11:54:25 -04:00

242 lines
8.8 KiB
TypeScript

import { logger } from '../utils/logger';
export interface CIMReviewData {
dealOverview: {
targetCompanyName: string;
industrySector: string;
geography: string;
dealSource: string;
transactionType: string;
dateCIMReceived: string;
dateReviewed: string;
reviewers: string;
cimPageCount: string;
statedReasonForSale: string;
employeeCount: string;
};
businessDescription: {
coreOperationsSummary: string;
keyProductsServices: string;
uniqueValueProposition: string;
customerBaseOverview: {
keyCustomerSegments: string;
customerConcentrationRisk: string;
typicalContractLength: string;
};
keySupplierOverview: {
dependenceConcentrationRisk: string;
};
};
marketIndustryAnalysis: {
estimatedMarketSize: string;
estimatedMarketGrowthRate: string;
keyIndustryTrends: string;
competitiveLandscape: {
keyCompetitors: string;
targetMarketPosition: string;
basisOfCompetition: string;
};
barriersToEntry: string;
};
financialSummary: {
financials: {
fy3: { revenue: string; revenueGrowth: string; grossProfit: string; grossMargin: string; ebitda: string; ebitdaMargin: string };
fy2: { revenue: string; revenueGrowth: string; grossProfit: string; grossMargin: string; ebitda: string; ebitdaMargin: string };
fy1: { revenue: string; revenueGrowth: string; grossProfit: string; grossMargin: string; ebitda: string; ebitdaMargin: string };
ltm: { revenue: string; revenueGrowth: string; grossProfit: string; grossMargin: string; ebitda: string; ebitdaMargin: string };
};
qualityOfEarnings: string;
revenueGrowthDrivers: string;
marginStabilityAnalysis: string;
capitalExpenditures: string;
workingCapitalIntensity: string;
freeCashFlowQuality: string;
};
managementTeamOverview: {
keyLeaders: string;
managementQualityAssessment: string;
postTransactionIntentions: string;
organizationalStructure: string;
};
preliminaryInvestmentThesis: {
keyAttractions: string;
potentialRisks: string;
valueCreationLevers: string;
alignmentWithFundStrategy: string;
};
keyQuestionsNextSteps: {
criticalQuestions: string;
missingInformation: string;
preliminaryRecommendation: string;
rationaleForRecommendation: string;
proposedNextSteps: string;
};
}
class CSVExportService {
/**
* Convert CIM Review data to CSV format
*/
static generateCIMReviewCSV(reviewData: CIMReviewData, companyName: string = 'Unknown'): string {
try {
const csvRows: string[] = [];
// Add header
csvRows.push('BPCP CIM Review Summary');
csvRows.push(`Company: ${companyName}`);
csvRows.push(`Generated: ${new Date().toISOString()}`);
csvRows.push(''); // Empty row
// Deal Overview Section
csvRows.push('DEAL OVERVIEW');
csvRows.push('Field,Value');
if (reviewData.dealOverview) {
Object.entries(reviewData.dealOverview).forEach(([key, value]) => {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
});
}
csvRows.push(''); // Empty row
// Business Description Section
csvRows.push('BUSINESS DESCRIPTION');
csvRows.push('Field,Value');
if (reviewData.businessDescription) {
Object.entries(reviewData.businessDescription).forEach(([key, value]) => {
if (typeof value === 'object') {
// Handle nested objects like customerBaseOverview
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
csvRows.push(`${this.formatFieldName(key)} - ${this.formatFieldName(nestedKey)},${this.escapeCSVValue(nestedValue)}`);
});
} else {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
}
});
}
csvRows.push(''); // Empty row
// Market & Industry Analysis Section
csvRows.push('MARKET & INDUSTRY ANALYSIS');
csvRows.push('Field,Value');
if (reviewData.marketIndustryAnalysis) {
Object.entries(reviewData.marketIndustryAnalysis).forEach(([key, value]) => {
if (typeof value === 'object') {
// Handle nested objects like competitiveLandscape
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
csvRows.push(`${this.formatFieldName(key)} - ${this.formatFieldName(nestedKey)},${this.escapeCSVValue(nestedValue)}`);
});
} else {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
}
});
}
csvRows.push(''); // Empty row
// Financial Summary Section
csvRows.push('FINANCIAL SUMMARY');
csvRows.push('Period,Revenue,Revenue Growth,Gross Profit,Gross Margin,EBITDA,EBITDA Margin');
if (reviewData.financialSummary?.financials) {
Object.entries(reviewData.financialSummary.financials).forEach(([period, financials]) => {
csvRows.push(`${period.toUpperCase()},${this.escapeCSVValue(financials.revenue)},${this.escapeCSVValue(financials.revenueGrowth)},${this.escapeCSVValue(financials.grossProfit)},${this.escapeCSVValue(financials.grossMargin)},${this.escapeCSVValue(financials.ebitda)},${this.escapeCSVValue(financials.ebitdaMargin)}`);
});
}
csvRows.push(''); // Empty row
// Additional Financial Metrics
csvRows.push('ADDITIONAL FINANCIAL METRICS');
csvRows.push('Field,Value');
if (reviewData.financialSummary) {
const additionalMetrics = [
'qualityOfEarnings',
'revenueGrowthDrivers',
'marginStabilityAnalysis',
'capitalExpenditures',
'workingCapitalIntensity',
'freeCashFlowQuality'
];
additionalMetrics.forEach(metric => {
if (reviewData.financialSummary[metric]) {
csvRows.push(`${this.formatFieldName(metric)},${this.escapeCSVValue(reviewData.financialSummary[metric])}`);
}
});
}
csvRows.push(''); // Empty row
// Management Team Overview Section
csvRows.push('MANAGEMENT TEAM OVERVIEW');
csvRows.push('Field,Value');
if (reviewData.managementTeamOverview) {
Object.entries(reviewData.managementTeamOverview).forEach(([key, value]) => {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
});
}
csvRows.push(''); // Empty row
// Preliminary Investment Thesis Section
csvRows.push('PRELIMINARY INVESTMENT THESIS');
csvRows.push('Field,Value');
if (reviewData.preliminaryInvestmentThesis) {
Object.entries(reviewData.preliminaryInvestmentThesis).forEach(([key, value]) => {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
});
}
csvRows.push(''); // Empty row
// Key Questions & Next Steps Section
csvRows.push('KEY QUESTIONS & NEXT STEPS');
csvRows.push('Field,Value');
if (reviewData.keyQuestionsNextSteps) {
Object.entries(reviewData.keyQuestionsNextSteps).forEach(([key, value]) => {
csvRows.push(`${this.formatFieldName(key)},${this.escapeCSVValue(value)}`);
});
}
return csvRows.join('\n');
} catch (error) {
logger.error('Failed to generate CSV from CIM Review data', { error });
throw new Error('Failed to generate CSV export');
}
}
/**
* Format field names for better readability
*/
private static formatFieldName(fieldName: string): string {
return fieldName
.replace(/([A-Z])/g, ' $1') // Add space before capital letters
.replace(/^./, str => str.toUpperCase()) // Capitalize first letter
.trim();
}
/**
* Escape CSV values to handle commas, quotes, and newlines
*/
private static escapeCSVValue(value: string): string {
if (!value) return '';
// Replace newlines with spaces and trim
const cleanValue = value.replace(/\n/g, ' ').replace(/\r/g, ' ').trim();
// If value contains comma, quote, or newline, wrap in quotes and escape internal quotes
if (cleanValue.includes(',') || cleanValue.includes('"') || cleanValue.includes('\n')) {
return `"${cleanValue.replace(/"/g, '""')}"`;
}
return cleanValue;
}
/**
* Generate standardized filename for CSV export
*/
static generateCSVFilename(companyName: string): string {
const date = new Date().toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
const sanitizedCompanyName = companyName
.replace(/[^a-zA-Z0-9\s]/g, '') // Remove special characters
.replace(/\s+/g, '_') // Replace spaces with underscores
.toUpperCase();
return `${date}_${sanitizedCompanyName}_CIM_Data.csv`;
}
}
export default CSVExportService;