import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config({ path: '.env' }); dotenv.config({ path: '.env.local' }); // Firebase Admin setup (if configured) let admin = null; let db = null; const isFirebaseConfigured = !!( process.env.VITE_FIREBASE_API_KEY && process.env.VITE_FIREBASE_AUTH_DOMAIN && process.env.VITE_FIREBASE_PROJECT_ID && process.env.VITE_FIREBASE_STORAGE_BUCKET && process.env.VITE_FIREBASE_MESSAGING_SENDER_ID && process.env.VITE_FIREBASE_APP_ID ); if (isFirebaseConfigured) { try { const { initializeApp } = await import('firebase-admin/app'); const { getFirestore } = await import('firebase-admin/firestore'); // Initialize Firebase Admin with project ID (no service account needed for Firestore in same project) admin = initializeApp({ projectId: process.env.VITE_FIREBASE_PROJECT_ID }); db = getFirestore(admin); console.log('✓ Firebase Admin initialized for project:', process.env.VITE_FIREBASE_PROJECT_ID); } catch (error) { console.log('⚠️ Firebase Admin setup failed:', error.message); console.log('Using in-memory storage for all data including invites'); } } else { console.log('⚠️ Firebase not configured, using in-memory storage for all data'); } // OpenAI setup (if API key is available) - handle import gracefully let openai = null; try { if (process.env.OPENAI_API_KEY) { const { OpenAI } = await import('openai'); openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); } } catch (e) { console.log('OpenAI not available, using mock responses'); } const app = express(); const PORT = process.env.PORT || 5050; const SITE_URL = process.env.VITE_SITE_URL || 'http://localhost:5173'; const API_URL = process.env.API_URL || 'http://localhost:5050'; // In-memory storage for demo mode - MULTI-TENANT AWARE const organizationData = new Map(); // orgId -> { submissions: Map, reports: Map, employees: Array } const userOrganizations = new Map(); // userId -> [{ orgId, role, joinedAt }] const organizationMetadata = new Map(); // orgId -> { name, ownerId, createdAt, onboardingCompleted } // Helper function to get or create organization data const getOrgData = (orgId) => { if (!organizationData.has(orgId)) { organizationData.set(orgId, { submissions: new Map(), // employeeId -> { employee, answers, submittedAt } reports: new Map(), // employeeId -> { report, generatedAt, employee } employees: [] // employee records }); } return organizationData.get(orgId); }; // Helper functions for user-organization relationships const getUserOrganizations = (userId) => { return userOrganizations.get(userId) || []; }; const addUserToOrganization = (userId, orgId, role = 'employee') => { const userOrgs = getUserOrganizations(userId); if (!userOrgs.find(org => org.orgId === orgId)) { userOrgs.push({ orgId, role, joinedAt: Date.now() }); userOrganizations.set(userId, userOrgs); } }; const createOrganization = (orgId, name, ownerId) => { organizationMetadata.set(orgId, { id: orgId, name, ownerId, createdAt: Date.now(), onboardingCompleted: false }); // Add owner to organization addUserToOrganization(ownerId, orgId, 'owner'); // Initialize org data getOrgData(orgId); return organizationMetadata.get(orgId); }; // Middleware app.use(cors()); app.use(express.json()); // Simple health check app.get('/api/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString() }); }); // User Organizations Management app.get('/api/user/:userId/organizations', (req, res) => { const { userId } = req.params; const userOrgs = getUserOrganizations(userId); // Enrich with organization metadata const enrichedOrgs = userOrgs.map(userOrg => { const orgMeta = organizationMetadata.get(userOrg.orgId); return { orgId: userOrg.orgId, name: orgMeta?.name || 'Unknown Organization', role: userOrg.role, onboardingCompleted: orgMeta?.onboardingCompleted || false, joinedAt: userOrg.joinedAt }; }); res.json({ organizations: enrichedOrgs }); }); app.post('/api/organizations', (req, res) => { const { name, userId } = req.body; if (!name || !userId) { return res.status(400).json({ error: 'Name and userId are required' }); } // Generate unique org ID const orgId = `org_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; try { const org = createOrganization(orgId, name, userId); res.json({ orgId, name: org.name, role: 'owner', onboardingCompleted: org.onboardingCompleted, joinedAt: Date.now() }); } catch (error) { console.error('Failed to create organization:', error); res.status(500).json({ error: 'Failed to create organization' }); } }); app.get('/api/organizations/:orgId', (req, res) => { const { orgId } = req.params; const orgMeta = organizationMetadata.get(orgId); if (!orgMeta) { return res.status(404).json({ error: 'Organization not found' }); } res.json(orgMeta); }); app.put('/api/organizations/:orgId', (req, res) => { const { orgId } = req.params; const updates = req.body; const orgMeta = organizationMetadata.get(orgId); if (!orgMeta) { return res.status(404).json({ error: 'Organization not found' }); } // Update organization metadata const updated = { ...orgMeta, ...updates }; organizationMetadata.set(orgId, updated); res.json(updated); }); // In-memory storage for OTP codes (in production, use Redis or database) const otpStorage = new Map(); // Generate random 6-digit OTP const generateOTP = () => Math.floor(100000 + Math.random() * 900000).toString(); // OTP Authentication endpoints app.post('/api/auth/send-otp', async (req, res) => { const { email, inviteCode } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } try { // Generate OTP const otp = generateOTP(); const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry // Store OTP (in production, use Redis with TTL) otpStorage.set(email, { otp, expiresAt, attempts: 0, inviteCode: inviteCode || null }); // In production, send actual email via SendGrid, AWS SES, etc. console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`); // Mock email sending setTimeout(() => { console.log(`✅ Email sent to ${email} with OTP: ${otp}`); }, 100); res.json({ success: true, message: 'Verification code sent to your email', // In development/demo mode, include OTP for testing (remove in production) ...(process.env.NODE_ENV !== 'production' && { otp }) }); } catch (error) { console.error('Send OTP error:', error); res.status(500).json({ error: 'Failed to send verification code' }); } }); app.post('/api/auth/verify-otp', async (req, res) => { const { email, otp, inviteCode } = req.body; if (!email || !otp) { return res.status(400).json({ error: 'Email and OTP are required' }); } try { const storedData = otpStorage.get(email); if (!storedData) { return res.status(400).json({ error: 'No verification code found for this email' }); } // Check expiry if (Date.now() > storedData.expiresAt) { otpStorage.delete(email); return res.status(400).json({ error: 'Verification code has expired' }); } // Check attempt limit if (storedData.attempts >= 3) { otpStorage.delete(email); return res.status(400).json({ error: 'Too many failed attempts' }); } // Verify OTP if (storedData.otp !== otp) { storedData.attempts++; return res.status(400).json({ error: 'Invalid verification code' }); } // Success - clear OTP and create user session otpStorage.delete(email); // Generate mock JWT token (in production, use proper JWT with secrets) const token = `mock_jwt_${Buffer.from(email).toString('base64')}_${Date.now()}`; // Mock user data const user = { uid: `user_${Buffer.from(email).toString('base64').slice(0, 8)}`, email, displayName: email.split('@')[0], emailVerified: true, createdAt: new Date().toISOString() }; res.json({ success: true, token, user, inviteCode: storedData.inviteCode }); } catch (error) { console.error('Verify OTP error:', error); res.status(500).json({ error: 'Failed to verify code' }); } }); // Employee report generation (LLM-powered when available) - MULTI-TENANT app.post('/api/employee-report', async (req, res) => { const { employeeId, orgId } = req.body; if (!employeeId || !orgId) { return res.status(400).json({ error: 'Employee ID and Organization ID are required' }); } // Get organization data const orgData = getOrgData(orgId); // Get stored submission data, or create demo data if none exists let submission = orgData.submissions.get(employeeId); if (!submission) { // Create demo submission data for testing const demoSubmissions = { 'AG': { employee: { name: 'Alex Green', role: 'Influencer Coordinator', department: 'Marketing' }, answers: { role_clarity: "I understand my role very clearly as Influencer Coordinator & Business Development Outreach.", key_outputs: "Recruited 15 new influencers, managed 8 campaigns, initiated 3 business development partnerships.", bottlenecks: "Campaign organization could be better, sometimes unclear on priorities between recruiting and outreach.", hidden_talent: "Strong relationship building skills that could be leveraged for client-facing work.", retention_risk: "Happy with the company but would like more structure and clearer processes.", energy_distribution: "50% influencer recruiting, 30% campaign support, 20% business development outreach.", performance_indicators: "Good influencer relationships, but delivery timeline improvements needed.", workflow: "Morning outreach, afternoon campaign work, weekly business development calls." } }, 'MB': { employee: { name: 'Michael Brown', role: 'Senior Developer', department: 'Engineering' }, answers: { role_clarity: "I understand my role as a Senior Developer very clearly. I'm responsible for architecting solutions, code reviews, and mentoring junior developers.", key_outputs: "Delivered 3 major features this quarter, reduced technical debt by 20%, and led code review process improvements.", bottlenecks: "Sometimes waiting for design specs from the product team, and occasional deployment pipeline issues.", hidden_talent: "I have strong business analysis skills and could help bridge the gap between technical and business requirements.", retention_risk: "I'm satisfied with my current role and compensation. The only concern would be limited growth opportunities.", energy_distribution: "80% development work, 15% mentoring, 5% planning and architecture.", performance_indicators: "Code quality metrics improved, zero production bugs in my recent releases, positive peer feedback.", workflow: "Morning standup, focused coding blocks, afternoon reviews and collaboration, weekly planning sessions." } } }; submission = demoSubmissions[employeeId]; if (submission) { // Store the demo submission for future use in this organization orgData.submissions.set(employeeId, { ...submission, orgId }); console.log(`Created demo submission for employee: ${submission.employee.name} in org: ${orgId}`); } } if (!submission) { return res.status(404).json({ error: 'Employee submission not found' }); } if (openai) { try { const { employee, answers } = submission; const prompt = `Based on the following employee questionnaire responses, generate a comprehensive employee performance report in JSON format. Employee Information: - Name: ${employee.name} - Role: ${employee.role || 'Unknown'} - Department: ${employee.department || 'General'} - Employee ID: ${employeeId} Questionnaire Responses: ${Object.entries(answers).map(([key, value]) => `- ${key}: ${value}`).join('\n')} Generate a JSON report with the following structure: { "employeeId": "${employeeId}", "employeeName": "${employee.name}", "department": "${employee.department || 'General'}", "role": "${employee.role || 'Unknown'}", "roleAndOutput": { "responsibilities": "string - based on role_clarity response", "clarityOnRole": "Clear|Needs definition - assessment based on responses", "selfRatedOutput": "string - based on key_outputs and performance indicators", "recurringTasks": "string - based on energy_distribution and workflow responses" }, "insights": { "personalityTraits": "string - inferred personality traits from responses", "psychologicalIndicators": ["array of psychological and behavioral indicators"], "selfAwareness": "string - assessment of employee's self-awareness level", "emotionalResponses": "string - emotional stability and resilience assessment", "growthDesire": "string - desire and capacity for professional growth", "strengths": ["array of identified strength areas from responses"], "weaknesses": ["array of areas needing improvement"] }, "strengths": ["array of key strengths based on all responses"], "weaknesses": [{"isCritical": boolean, "description": "string - specific weakness with severity"}], "opportunities": { "roleAdjustment": "string - suggested role or responsibility adjustments", "accountabilitySupport": "string - recommendations for better accountability and support" }, "risks": ["array of potential retention, performance, or engagement risks"], "recommendation": { "action": "Keep|Review|Develop|Reassign", "details": ["array of specific, actionable recommendations"], "priority": "High|Medium|Low", "timeline": "string - suggested timeline for actions" }, "grading": [{ "department": "${employee.department || 'General'}", "lead": "Manager/Lead name or TBD", "support": "Support team or resources needed", "grade": "A+|A|A-|B+|B|B-|C+|C|C-|D+|D|F", "comment": "string - detailed grading rationale", "scores": [ {"subject": "Output Quality", "value": number, "fullMark": 100}, {"subject": "Role Clarity", "value": number, "fullMark": 100}, {"subject": "Growth Trajectory", "value": number, "fullMark": 100}, {"subject": "Mission Alignment", "value": number, "fullMark": 100}, {"subject": "Retention Risk", "value": number, "fullMark": 100} ] }], "suitabilityScore": number, // 0-100 overall suitability for current role "retentionRisk": "Low|Medium|High", "costEffectiveness": "High Value|Good Value|Fair Value|Poor Value", "generatedAt": "${new Date().toISOString()}", "dataSource": "Employee Questionnaire" } IMPORTANT: Provide thoughtful, specific analysis based on the actual questionnaire responses. Be professional, constructive, and actionable in all recommendations. Consider the employee's self-assessment and match it with objective indicators.`; const completion = await openai.chat.completions.create({ model: 'gpt-4.1-nano', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, temperature: 0.7, max_tokens: 2000 }); const reportData = JSON.parse(completion.choices[0].message.content); console.log('✓ LLM employee report generated successfully for:', employee.name); // Store the generated report for this organization orgData.reports.set(employeeId, { report: reportData, generatedAt: Date.now(), employee: employee, orgId: orgId }); return res.json({ success: true, report: reportData, generatedAt: Date.now(), source: 'LLM' }); } catch (error) { console.error('❌ LLM employee report generation failed:', error.message); console.log('Falling back to mock report generation...'); // Fall through to mock generation } } // Mock report generation as fallback const { employee, answers } = submission; const mockReport = { employeeId: employeeId, employeeName: employee.name, department: employee.department || 'General', role: employee.role || 'Unknown', roleAndOutput: { responsibilities: "Role responsibilities based on questionnaire responses", clarityOnRole: "Clear", selfRatedOutput: "Good performance indicators", recurringTasks: "Regular task execution" }, insights: { personalityTraits: "Professional, dedicated, growth-oriented", psychologicalIndicators: ["Self-motivated", "Team-oriented", "Detail-focused"], selfAwareness: "High level of self-awareness demonstrated", emotionalResponses: "Stable and professional emotional responses", growthDesire: "Strong desire for professional development", strengths: ["Communication", "Problem-solving", "Adaptability"], weaknesses: ["Time management", "Delegation"] }, strengths: ["Strong technical skills", "Good communication", "Reliable performance"], weaknesses: [ { "isCritical": false, "description": "Could improve time management" }, { "isCritical": false, "description": "Needs more leadership development" } ], opportunities: { roleAdjustment: "Consider expanded responsibilities in current role", accountabilitySupport: "Regular check-ins and goal setting recommended" }, risks: ["Low retention risk", "No immediate performance concerns"], recommendation: { action: "Keep", details: ["Continue current performance", "Provide growth opportunities"], priority: "Medium", timeline: "Quarterly review recommended" }, grading: [{ department: employee.department || 'General', lead: "Department Manager", support: "HR and direct supervisor", grade: "B+", comment: "Solid performer with good potential for growth", scores: [ { "subject": "Output Quality", "value": 85, "fullMark": 100 }, { "subject": "Role Clarity", "value": 80, "fullMark": 100 }, { "subject": "Growth Trajectory", "value": 75, "fullMark": 100 }, { "subject": "Mission Alignment", "value": 82, "fullMark": 100 }, { "subject": "Retention Risk", "value": 90, "fullMark": 100 } ] }], suitabilityScore: 82, retentionRisk: "Low", costEffectiveness: "Good Value", generatedAt: new Date().toISOString(), dataSource: "Mock Report (LLM unavailable)" }; console.log('✓ Mock employee report generated for:', employee.name); // Store the mock report for this organization orgData.reports.set(employeeId, { report: mockReport, generatedAt: Date.now(), employee: employee, orgId: orgId }); return res.json({ success: true, report: mockReport, generatedAt: Date.now(), source: 'Mock' }); }); // Get employee report - MULTI-TENANT app.get('/api/employee-report/:orgId/:employeeId', (req, res) => { const { employeeId, orgId } = req.params; const orgData = getOrgData(orgId); const storedReport = orgData.reports.get(employeeId); if (!storedReport) { return res.status(404).json({ error: 'Employee report not found' }); } console.log(`✓ Retrieved employee report for: ${storedReport.employee.name} in org: ${orgId}`); return res.json({ success: true, report: storedReport.report, generatedAt: storedReport.generatedAt, employee: storedReport.employee }); }); // List all employee reports for an organization - MULTI-TENANT app.get('/api/employee-reports/:orgId', (req, res) => { const { orgId } = req.params; if (!orgId) { return res.status(400).json({ error: 'Organization ID is required' }); } const orgData = getOrgData(orgId); console.log(`Fetching reports for organization: ${orgId}`); const reports = Array.from(orgData.reports.entries()).map(([employeeId, data]) => ({ employeeId, employeeName: data.employee.name, department: data.employee.department, role: data.employee.role, generatedAt: data.generatedAt, hasReport: true })); console.log(`✓ Retrieved ${reports.length} employee reports for org: ${orgId}`); return res.json({ success: true, reports: reports, total: reports.length }); }); // Invite management - Firebase + in-memory fallback const invites = new Map(); // For demo mode: code -> { employee, used, createdAt, orgId } function generateInviteCode() { return Math.random().toString(36).slice(2, 10); } // Create invite endpoint app.post('/api/invitations', async (req, res) => { try { const { name, email, role, department, orgId } = req.body || {}; if (!name || !email) return res.status(400).json({ error: 'name and email required' }); if (!orgId) return res.status(400).json({ error: 'orgId required' }); const code = generateInviteCode(); const employeeId = (name.split(/\s+/).map(s => s[0]).join('').slice(0, 4) || 'EMP').toUpperCase(); const employee = { id: employeeId, name, email, role, department }; const inviteData = { employee, used: false, createdAt: Date.now(), orgId, code }; // Store in Firebase if configured, otherwise in memory if (db) { await db.collection('orgs').doc(orgId).collection('invites').doc(code).set(inviteData); console.log(`✓ Invite stored in Firestore: ${code} for ${name} in org: ${orgId}`); } else { invites.set(code, inviteData); console.log(`✓ Invite stored in memory: ${code} for ${name}`); } res.json({ code, inviteLink: `${SITE_URL}/#/invite/${code}`, emailLink: `${API_URL}/invite/${code}`, employee }); } catch (error) { console.error('Error creating invite:', error); res.status(500).json({ error: 'Failed to create invite' }); } }); // Get invite details endpoint app.get('/api/invitations/:code', async (req, res) => { try { const code = req.params.code; let inviteData = null; // Check Firebase first if configured if (db) { // Search across all orgs for this invite code const orgsSnapshot = await db.collection('orgs').get(); for (const orgDoc of orgsSnapshot.docs) { const inviteDoc = await db.collection('orgs').doc(orgDoc.id).collection('invites').doc(code).get(); if (inviteDoc.exists) { inviteData = inviteDoc.data(); break; } } } else { // Fallback to in-memory storage inviteData = invites.get(code); } if (!inviteData) { return res.status(404).json({ error: 'invalid invite' }); } res.json({ code, employee: inviteData.employee, used: inviteData.used, orgId: inviteData.orgId }); } catch (error) { console.error('Error getting invite:', error); res.status(500).json({ error: 'Failed to get invite details' }); } }); // GET endpoint for invite redirect (for email links that need GET) app.get('/invite/:code', async (req, res) => { try { const code = req.params.code; let inviteData = null; // Check Firebase first if configured if (db) { const orgsSnapshot = await db.collection('orgs').get(); for (const orgDoc of orgsSnapshot.docs) { const inviteDoc = await db.collection('orgs').doc(orgDoc.id).collection('invites').doc(code).get(); if (inviteDoc.exists) { inviteData = inviteDoc.data(); break; } } } else { inviteData = invites.get(code); } if (!inviteData) { return res.status(404).send(` Invalid Invite

Invalid Invite

This invite link is invalid or has expired.

Go to Login `); } if (inviteData.used) { return res.status(410).send(` Invite Already Used

Invite Already Used

This invite has already been accepted.

Go to Login `); } // Redirect to the frontend app with the invite code res.redirect(`${SITE_URL}/#/invite/${code}`); } catch (error) { console.error('Error handling invite redirect:', error); res.status(500).send(` Error

Error

An error occurred while processing your invite.

Go to Login `); } }); // Consume invite endpoint app.post('/api/invitations/:code/consume', async (req, res) => { try { const code = req.params.code; let inviteData = null; let orgId = null; // Check Firebase first if configured if (db) { const orgsSnapshot = await db.collection('orgs').get(); for (const orgDoc of orgsSnapshot.docs) { const inviteRef = db.collection('orgs').doc(orgDoc.id).collection('invites').doc(code); const inviteDoc = await inviteRef.get(); if (inviteDoc.exists) { inviteData = inviteDoc.data(); orgId = orgDoc.id; // Mark as used await inviteRef.update({ used: true }); console.log(`✓ Invite consumed in Firestore: ${code} in org: ${orgId}`); break; } } } else { // Fallback to in-memory storage inviteData = invites.get(code); if (inviteData) { inviteData.used = true; invites.set(code, inviteData); orgId = inviteData.orgId; console.log(`✓ Invite consumed in memory: ${code}`); } } if (!inviteData) { return res.status(404).json({ error: 'invalid invite' }); } if (inviteData.used && !db) { // Only check if not already updated in Firebase return res.status(410).json({ error: 'invite already used' }); } res.json({ employee: inviteData.employee, consumedAt: Date.now(), orgId }); } catch (error) { console.error('Error consuming invite:', error); res.status(500).json({ error: 'Failed to consume invite' }); } }); // In-memory submissions store (demo mode only) const submissions = new Map(); // employeeId -> { answers, submittedAt } // Employee questionnaire submission endpoint app.post('/api/employee-submissions', async (req, res) => { try { const { employeeId, employee, answers, orgId } = req.body; if (!employeeId || !answers || !orgId) { return res.status(400).json({ error: 'Missing required fields: employeeId, answers, and orgId are required' }); } // Get organization data const orgData = getOrgData(orgId); // Store the submission for this organization orgData.submissions.set(employeeId, { employeeId, orgId, employee: employee || { name: 'Unknown', email: 'unknown@example.com' }, answers, submittedAt: Date.now() }); console.log(`✓ Employee questionnaire submitted for: ${employee?.name || employeeId} in org: ${orgId}`); console.log(`Total submissions for org ${orgId}: ${orgData.submissions.size}`); // Automatically generate report after submission if (openai) { try { console.log('🤖 Generating AI report for employee...'); const prompt = `Based on the following employee questionnaire responses, generate a comprehensive employee performance report in JSON format. Employee Information: - Name: ${employee?.name || 'Unknown'} - Role: ${employee?.role || 'Unknown'} - Department: ${employee?.department || 'General'} - Employee ID: ${employeeId} - Organization ID: ${orgId} Questionnaire Responses: ${Object.entries(answers).map(([key, value]) => `- ${key}: ${value}`).join('\n')} Generate a JSON report with the following structure: { "employeeId": "${employeeId}", "employeeName": "${employee?.name || 'Unknown'}", "department": "${employee?.department || 'General'}", "role": "${employee?.role || 'Unknown'}", "roleAndOutput": { "responsibilities": "string - based on role_clarity response", "clarityOnRole": "Clear|Needs definition - assessment based on responses", "selfRatedOutput": "string - based on key_outputs and performance indicators", "recurringTasks": "string - based on energy_distribution and workflow responses" }, "insights": { "personalityTraits": "string - inferred personality traits from responses", "psychologicalIndicators": ["array of psychological and behavioral indicators"], "selfAwareness": "string - assessment of employee's self-awareness level", "emotionalResponses": "string - emotional stability and resilience assessment", "growthDesire": "string - desire and capacity for professional growth", "strengths": ["array of identified strength areas from responses"], "weaknesses": ["array of areas needing improvement"] }, "strengths": ["array of key strengths based on all responses"], "weaknesses": [{"isCritical": boolean, "description": "string - specific weakness with severity"}], "opportunities": { "roleAdjustment": "string - suggested role or responsibility adjustments", "accountabilitySupport": "string - recommendations for better accountability and support" }, "risks": ["array of potential retention, performance, or engagement risks"], "recommendation": { "action": "Keep|Review|Develop|Reassign", "details": ["array of specific, actionable recommendations"], "priority": "High|Medium|Low", "timeline": "string - suggested timeline for actions" }, "grading": [{ "department": "${employee?.department || 'General'}", "lead": "Manager/Lead name or TBD", "support": "Support team or resources needed", "grade": "A+|A|A-|B+|B|B-|C+|C|C-|D+|D|F", "comment": "string - detailed grading rationale", "scores": [ {"subject": "Output Quality", "value": number, "fullMark": 100}, {"subject": "Role Clarity", "value": number, "fullMark": 100}, {"subject": "Growth Trajectory", "value": number, "fullMark": 100}, {"subject": "Mission Alignment", "value": number, "fullMark": 100}, {"subject": "Retention Risk", "value": number, "fullMark": 100} ] }], "suitabilityScore": number, "retentionRisk": "Low|Medium|High", "costEffectiveness": "High Value|Good Value|Fair Value|Poor Value", "generatedAt": "${new Date().toISOString()}", "dataSource": "Employee Questionnaire" } IMPORTANT: Provide thoughtful, specific analysis based on the actual questionnaire responses. Be professional, constructive, and actionable in all recommendations.`; const completion = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, temperature: 0.7, max_tokens: 2000 }); const reportData = JSON.parse(completion.choices[0].message.content); // Store the generated report employeeReports.set(employeeId, { report: reportData, generatedAt: Date.now(), employee: employee || { name: 'Unknown', email: 'unknown@example.com' } }); console.log(`✓ AI report generated and stored for: ${employee?.name || employeeId}`); return res.json({ success: true, employeeId, submittedAt: Date.now(), message: 'Questionnaire submitted and AI report generated successfully', reportGenerated: true }); } catch (llmError) { console.error('❌ LLM report generation failed:', llmError.message); console.log('📝 Questionnaire submitted successfully, report generation failed'); return res.json({ success: true, employeeId, submittedAt: Date.now(), message: 'Questionnaire submitted successfully (report generation failed)', reportGenerated: false }); } } else { console.log('📝 Questionnaire submitted successfully (LLM not configured)'); return res.json({ success: true, employeeId, submittedAt: Date.now(), message: 'Questionnaire submitted successfully', reportGenerated: false }); } } catch (error) { console.error('❌ Error processing employee submission:', error); return res.status(500).json({ error: 'Failed to process submission' }); } }); // Company wiki endpoint (wraps company report + raw onboarding Q&A style wiki) app.post('/api/company-wiki', (req, res) => { const { org = {}, submissions = {} } = req.body || {}; console.log('Received company-wiki request with org data:', org); // Basic derived company report subset (reuse logic simplistically) const departmentBreakdown = [ { department: 'Engineering', count: 2 }, { department: 'Marketing', count: 2 }, { department: 'Operations', count: 1 }, { department: 'HR', count: 1 } ]; const report = { id: 'cw_' + Date.now(), createdAt: Date.now(), overview: { totalEmployees: Object.keys(submissions).length || 6, departmentBreakdown, submissionRate: 75, lastUpdated: Date.now(), averagePerformanceScore: 4.2, riskLevel: 'Medium' }, keyPersonnelChanges: [], immediateHiringNeeds: [], forwardOperatingPlan: { quarterlyGoals: [org.shortTermGoals || 'Drive focused growth'], resourceNeeds: ['Structured process improvements'], riskMitigation: ['Knowledge sharing cadence'] }, organizationalStrengths: [org.advantages || 'Mission-aligned core team'].filter(Boolean), organizationalRisks: [org.vulnerabilities || 'Key-person dependencies'].filter(Boolean), gradingOverview: departmentBreakdown.map(d => ({ department: d.department, averageScore: 4 + Math.random(), totalEmployees: d.count, scores: [] })), executiveSummary: `${org.name || 'Company'} is a ${org.industry || 'technology'} company with ${org.size || '11-50'} employees. Mission: ${org.mission || 'N/A'} | Vision: ${org.vision || 'N/A'}` }; const wiki = { sections: [ { question: 'Mission', answer: org.mission || '' }, { question: 'Vision', answer: org.vision || '' }, { question: 'Evolution', answer: org.evolution || '' }, { question: 'Competitive Advantages', answer: org.advantages || '' }, { question: 'Vulnerabilities', answer: org.vulnerabilities || '' }, { question: 'Short Term Goals', answer: org.shortTermGoals || '' }, { question: 'Long Term Goals', answer: org.longTermGoals || '' }, { question: 'Culture', answer: org.cultureDescription || '' }, { question: 'Work Environment', answer: org.workEnvironment || '' }, { question: 'Additional Context', answer: org.additionalContext || '' } ].filter(s => s.answer) }; console.log('Sending company-wiki response'); res.json({ report, wiki }); }); // Mock individual basic report endpoint (legacy) retained for compatibility app.post('/api/report', (req, res) => { const mockReport = { ok: true }; setTimeout(() => res.json({ report: mockReport }), 300); }); // Mock company report endpoint (aggregates employee questionnaire data + org data) app.post('/api/company-report', (req, res) => { const org = req.body.org || {}; const employees = req.body.employees || []; // Gather all submission data const submissionData = Array.from(submissions.values()); const submissionCount = submissionData.length; const totalEmployees = employees.length; // Analyze common themes from submissions const commonBottlenecks = submissionData .map(s => s.answers.bottlenecks) .filter(Boolean) .slice(0, 3); const retentionRisks = submissionData .map(s => s.answers.retention_risk) .filter(Boolean) .slice(0, 3); const underutilizedTalents = submissionData .map(s => s.answers.hidden_talent) .filter(Boolean) .slice(0, 3); const departmentBreakdown = [ { department: 'Engineering', count: employees.filter(e => e.department === 'Engineering').length || 2 }, { department: 'Marketing', count: employees.filter(e => e.department === 'Marketing').length || 2 }, { department: 'Operations', count: employees.filter(e => e.department === 'Operations').length || 1 }, { department: 'HR', count: employees.filter(e => e.department === 'HR').length || 1 } ].filter(d => d.count > 0); const gradingCategories = [ { category: 'Execution', value: 78, rationale: 'Consistent delivery based on employee feedback' }, { category: 'People', value: submissionCount > 0 ? 85 : 70, rationale: `${submissionCount}/${totalEmployees} employees provided feedback` }, { category: 'Strategy', value: 72, rationale: 'Need clearer prioritization per employee input' }, { category: 'Risk', value: retentionRisks.length > 2 ? 65 : 75, rationale: `${retentionRisks.length} retention concerns identified` } ]; const legacyOverview = gradingCategories.reduce((acc, g) => { acc[g.category.toLowerCase()] = Math.round((g.value / 100) * 5 * 10) / 10; return acc; }, {}); const report = { id: `report-${Date.now()}`, createdAt: Date.now(), overview: { totalEmployees, departmentBreakdown, submissionRate: totalEmployees > 0 ? Math.round((submissionCount / totalEmployees) * 100) : 0, lastUpdated: Date.now(), averagePerformanceScore: 4.1, riskLevel: retentionRisks.length > 2 ? 'High' : retentionRisks.length > 0 ? 'Medium' : 'Low' }, personnelChanges: { newHires: [{ name: 'Alex Green', department: 'Marketing', role: 'Influencer Coordinator', impact: 'Expands outreach capability' }], promotions: [], departures: [] }, keyPersonnelChanges: [ { employeeName: 'Alex Green', role: 'Influencer Coordinator', department: 'Marketing', changeType: 'newHire', impact: 'Expands outreach capability' } ], immediateHiringNeeds: [ { department: 'Engineering', role: 'Senior Developer', priority: 'High', reasoning: 'Roadmap acceleration', urgency: 'high' }, { department: 'Marketing', role: 'Content Creator', priority: 'Medium', reasoning: 'Content velocity', urgency: 'medium' } ], operatingPlan: { nextQuarterGoals: [org.shortTermGoals || 'Improve collaboration', 'Expand market presence', 'Strengthen documentation'], keyInitiatives: ['Launch structured onboarding', 'Implement objective tracking'], resourceNeeds: ['Senior engineering capacity', 'Automation tooling'], riskMitigation: commonBottlenecks.length > 0 ? [`Address: ${commonBottlenecks[0]}`, 'Cross-training plan'] : ['Cross-training plan', 'Process retros'] }, forwardOperatingPlan: { quarterlyGoals: [org.shortTermGoals || 'Improve collaboration'], resourceNeeds: ['Senior engineering capacity'], riskMitigation: commonBottlenecks.length > 0 ? [`Address: ${commonBottlenecks[0]}`] : ['Cross-training plan'] }, organizationalStrengths: [ { area: org.advantages || 'Technical depth', description: 'Core team demonstrates strong execution speed.' }, { area: 'Culture', description: org.cultureDescription || 'Collaborative, learning oriented.' }, ...(underutilizedTalents.length > 0 ? [{ area: 'Hidden Talent', description: `Underutilized capabilities: ${underutilizedTalents.join(', ')}` }] : []) ], organizationalRisks: [ ...(retentionRisks.length > 0 ? retentionRisks.map(risk => `Retention risk: ${risk}`) : []), ...(commonBottlenecks.length > 0 ? [`Common bottleneck: ${commonBottlenecks[0]}`] : []), org.vulnerabilities || 'Process maturity gaps' ].filter(Boolean), organizationalImpactSummary: `Team impact is strong relative to size (${submissionCount}/${totalEmployees} employees surveyed); scaling processes will unlock further leverage.`, gradingBreakdown: gradingCategories, gradingOverview: legacyOverview, executiveSummary: generateExecutiveSummary(org, submissionData) }; setTimeout(() => res.json(report), 1200); }); function generateExecutiveSummary(org, submissionData = []) { const companyName = org.name || 'the organization'; const industry = org.industry || 'their industry'; const submissionCount = submissionData.length; let performanceInsights = ''; if (submissionCount > 0) { const topOutputs = submissionData .map(s => s.answers.key_outputs) .filter(Boolean) .slice(0, 2); const commonChallenges = submissionData .map(s => s.answers.bottlenecks) .filter(Boolean) .slice(0, 1); performanceInsights = ` Based on employee questionnaire data from ${submissionCount} team members, ` + `key outputs include ${topOutputs.length > 0 ? topOutputs[0] : 'various departmental deliverables'}.` + (commonChallenges.length > 0 ? ` Common challenges identified: ${commonChallenges[0]}.` : ''); } return `${companyName} is a ${org.size || 'growing'} company in the ${industry} sector with strong foundational elements in place. ` + `The organization's mission to "${org.mission || 'deliver value to customers'}" is supported by ${org.evolution || 'a history of adaptation and growth'}.${performanceInsights} ` + `Key competitive advantages include ${org.advantages || 'technical expertise and market positioning'}, though areas for improvement include ${org.vulnerabilities || 'process optimization and team scaling'}. ` + `Looking forward, the company is well-positioned to achieve its goals of ${org.shortTermGoals || 'continued growth and market expansion'} through strategic investments in talent and operational excellence.`; } // Mock chat endpoint app.post('/api/chat', (req, res) => { console.log('Chat request:', req.body.message); const responses = [ 'Based on the employee data, I can see that your team has strong technical skills.', 'The recent performance reviews indicate good collaboration across departments.', 'I notice some opportunities for growth in leadership development.', 'Your team seems well-balanced with a good mix of experience levels.' ]; const randomResponse = responses[Math.floor(Math.random() * responses.length)]; setTimeout(() => { res.json({ message: randomResponse, timestamp: new Date().toISOString() }); }, 800); }); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log('Mock API server ready for demo'); }); // (Export for potential tests) // module.exports = app;