diff --git a/backend/src/routes/documents.ts b/backend/src/routes/documents.ts
index f02d1c1..67351eb 100644
--- a/backend/src/routes/documents.ts
+++ b/backend/src/routes/documents.ts
@@ -90,6 +90,116 @@ router.get('/:id', validateUUID('id'), documentController.getDocument);
router.get('/:id/progress', validateUUID('id'), documentController.getDocumentProgress);
router.delete('/:id', validateUUID('id'), documentController.deleteDocument);
+// CIM Review data endpoints
+router.post('/:id/review', validateUUID('id'), async (req, res) => {
+ try {
+ const userId = req.user?.uid;
+ if (!userId) {
+ return res.status(401).json({
+ error: 'User not authenticated',
+ correlationId: req.correlationId
+ });
+ }
+
+ const { id } = req.params;
+ const reviewData = req.body;
+
+ if (!reviewData) {
+ return res.status(400).json({
+ error: 'Review data is required',
+ correlationId: req.correlationId
+ });
+ }
+
+ // Check if document exists and user has access
+ const document = await DocumentModel.findById(id);
+ if (!document) {
+ return res.status(404).json({
+ error: 'Document not found',
+ correlationId: req.correlationId
+ });
+ }
+
+ if (document.user_id !== userId) {
+ return res.status(403).json({
+ error: 'Access denied',
+ correlationId: req.correlationId
+ });
+ }
+
+ // Update the document with new analysis data
+ await DocumentModel.updateAnalysisResults(id, reviewData);
+
+ logger.info('CIM Review data saved successfully', {
+ documentId: id,
+ userId,
+ correlationId: req.correlationId
+ });
+
+ return res.json({
+ success: true,
+ message: 'CIM Review data saved successfully',
+ correlationId: req.correlationId || undefined
+ });
+
+ } catch (error) {
+ logger.error('Failed to save CIM Review data', {
+ error,
+ correlationId: req.correlationId
+ });
+ return res.status(500).json({
+ error: 'Failed to save CIM Review data',
+ correlationId: req.correlationId || undefined
+ });
+ }
+});
+
+router.get('/:id/review', validateUUID('id'), async (req, res) => {
+ try {
+ const userId = req.user?.uid;
+ if (!userId) {
+ return res.status(401).json({
+ error: 'User not authenticated',
+ correlationId: req.correlationId
+ });
+ }
+
+ const { id } = req.params;
+
+ // Check if document exists and user has access
+ const document = await DocumentModel.findById(id);
+ if (!document) {
+ return res.status(404).json({
+ error: 'Document not found',
+ correlationId: req.correlationId
+ });
+ }
+
+ if (document.user_id !== userId) {
+ return res.status(403).json({
+ error: 'Access denied',
+ correlationId: req.correlationId
+ });
+ }
+
+ return res.json({
+ success: true,
+ reviewData: document.analysis_data || {},
+ correlationId: req.correlationId || undefined
+ });
+
+ } catch (error) {
+ logger.error('Failed to get CIM Review data', {
+ error,
+ correlationId: req.correlationId
+ });
+ return res.status(500).json({
+ error: 'Failed to get CIM Review data',
+ correlationId: req.correlationId || undefined
+ });
+ }
+});
+
// Download endpoint (keeping this)
router.get('/:id/download', validateUUID('id'), async (req, res) => {
try {
@@ -144,8 +254,17 @@ router.get('/:id/download', validateUUID('id'), async (req, res) => {
});
}
+ // Generate standardized filename
+ const companyName = document.analysis_data?.dealOverview?.targetCompanyName || 'Unknown';
+ 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();
+ const filename = `${date}_${sanitizedCompanyName}_CIM_Review.pdf`;
+
res.setHeader('Content-Type', 'application/pdf');
- res.setHeader('Content-Disposition', `attachment; filename="${document.original_file_name.replace(/\.[^/.]+$/, '')}_cim_review.pdf"`);
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('x-correlation-id', req.correlationId || 'unknown');
return res.send(pdfBuffer);
@@ -172,6 +291,84 @@ router.get('/:id/download', validateUUID('id'), async (req, res) => {
}
});
+// CSV Export endpoint
+router.get('/:id/export-csv', validateUUID('id'), async (req, res) => {
+ try {
+ const userId = req.user?.uid;
+ if (!userId) {
+ return res.status(401).json({
+ error: 'User not authenticated',
+ correlationId: req.correlationId
+ });
+ }
+
+ const { id } = req.params;
+ if (!id) {
+ return res.status(400).json({
+ error: 'Document ID is required',
+ correlationId: req.correlationId
+ });
+ }
+
+ const document = await DocumentModel.findById(id);
+
+ if (!document) {
+ return res.status(404).json({
+ error: 'Document not found',
+ correlationId: req.correlationId
+ });
+ }
+
+ if (document.user_id !== userId) {
+ return res.status(403).json({
+ error: 'Access denied',
+ correlationId: req.correlationId
+ });
+ }
+
+ // Check if document has analysis data
+ if (!document.analysis_data) {
+ return res.status(404).json({
+ error: 'No analysis data available for CSV export',
+ correlationId: req.correlationId
+ });
+ }
+
+ // Generate CSV
+ try {
+ const { default: CSVExportService } = await import('../services/csvExportService');
+ const companyName = document.analysis_data?.dealOverview?.targetCompanyName || 'Unknown';
+ const csvContent = CSVExportService.generateCIMReviewCSV(document.analysis_data, companyName);
+ const filename = CSVExportService.generateCSVFilename(companyName);
+
+ res.setHeader('Content-Type', 'text/csv');
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
+ res.setHeader('x-correlation-id', req.correlationId || 'unknown');
+ return res.send(csvContent);
+
+ } catch (csvError) {
+ logger.error('CSV generation failed', {
+ error: csvError,
+ correlationId: req.correlationId
+ });
+ return res.status(500).json({
+ error: 'CSV generation failed',
+ correlationId: req.correlationId || undefined
+ });
+ }
+
+ } catch (error) {
+ logger.error('CSV export failed', {
+ error,
+ correlationId: req.correlationId
+ });
+ return res.status(500).json({
+ error: 'CSV export failed',
+ correlationId: req.correlationId || undefined
+ });
+ }
+});
+
// ONLY OPTIMIZED AGENTIC RAG PROCESSING ROUTE - All other processing routes disabled
router.post('/:id/process-optimized-agentic-rag', validateUUID('id'), async (req, res) => {
try {
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/App.tsx b/frontend/src/App.tsx
index bede9ae..e330430 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,6 +10,7 @@ import Analytics from './components/Analytics';
import UploadMonitoringDashboard from './components/UploadMonitoringDashboard';
import LogoutButton from './components/LogoutButton';
import { documentService, GCSErrorHandler, GCSError } from './services/documentService';
+import { adminService } from './services/adminService';
// import { debugAuth, testAPIAuth } from './utils/authDebug';
import {
@@ -34,6 +35,9 @@ const Dashboard: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState<'overview' | 'documents' | 'upload' | 'analytics' | 'monitoring'>('overview');
+ // Check if user is admin
+ const isAdmin = adminService.isAdmin(user?.email);
+
// Map backend status to frontend status
const mapBackendStatus = (backendStatus: string): string => {
switch (backendStatus) {
@@ -466,30 +470,34 @@ const Dashboard: React.FC = () => {
You don't have permission to view analytics.
+You don't have permission to view monitoring.
+