diff --git a/backend/src/services/csvExportService.ts b/backend/src/services/csvExportService.ts new file mode 100644 index 0000000..ded0306 --- /dev/null +++ b/backend/src/services/csvExportService.ts @@ -0,0 +1,242 @@ +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; \ No newline at end of file diff --git a/frontend/src/services/adminService.ts b/frontend/src/services/adminService.ts new file mode 100644 index 0000000..0d58da7 --- /dev/null +++ b/frontend/src/services/adminService.ts @@ -0,0 +1,93 @@ +import { apiClient } from './apiClient'; + +export interface AdminUser { + id: string; + email: string; + name: string; + role: 'user' | 'admin'; + createdAt: string; + lastLogin?: string; + isActive: boolean; +} + +export interface UserActivity { + userId: string; + email: string; + name: string; + loginCount: number; + lastLogin: string; + documentsProcessed: number; + totalProcessingTime: number; + averageProcessingTime: number; +} + +export interface SystemMetrics { + totalUsers: number; + activeUsers: number; + totalDocuments: number; + documentsProcessed: number; + averageProcessingTime: number; + successRate: number; + totalCost: number; + systemUptime: number; +} + +class AdminService { + private readonly ADMIN_EMAIL = 'jpressnell@bluepointcapital.com'; + + /** + * Check if current user is admin + */ + isAdmin(userEmail?: string): boolean { + return userEmail === this.ADMIN_EMAIL; + } + + /** + * Get all users (admin only) + */ + async getUsers(): Promise { + const response = await apiClient.get('/admin/users'); + return response.data.users; + } + + /** + * Get user activity statistics (admin only) + */ + async getUserActivity(): Promise { + const response = await apiClient.get('/admin/user-activity'); + return response.data.activity; + } + + /** + * Get system metrics (admin only) + */ + async getSystemMetrics(): Promise { + const response = await apiClient.get('/admin/system-metrics'); + return response.data.metrics; + } + + /** + * Get enhanced analytics (admin only) + */ + async getEnhancedAnalytics(days: number = 30): Promise { + const response = await apiClient.get(`/admin/enhanced-analytics?days=${days}`); + return response.data; + } + + /** + * Get weekly summary report (admin only) + */ + async getWeeklySummary(): Promise { + const response = await apiClient.get('/admin/weekly-summary'); + return response.data; + } + + /** + * Send weekly summary email (admin only) + */ + async sendWeeklySummaryEmail(): Promise { + await apiClient.post('/admin/send-weekly-summary'); + } +} + +export const adminService = new AdminService(); \ No newline at end of file diff --git a/to-do.md b/to-do.md new file mode 100644 index 0000000..e69de29