1163 lines
47 KiB
JavaScript
1163 lines
47 KiB
JavaScript
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(`
|
|
<html>
|
|
<head><title>Invalid Invite</title></head>
|
|
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
<h1>Invalid Invite</h1>
|
|
<p>This invite link is invalid or has expired.</p>
|
|
<a href="${SITE_URL}/#/login">Go to Login</a>
|
|
</body>
|
|
</html>
|
|
`);
|
|
}
|
|
|
|
if (inviteData.used) {
|
|
return res.status(410).send(`
|
|
<html>
|
|
<head><title>Invite Already Used</title></head>
|
|
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
<h1>Invite Already Used</h1>
|
|
<p>This invite has already been accepted.</p>
|
|
<a href="${SITE_URL}/#/login">Go to Login</a>
|
|
</body>
|
|
</html>
|
|
`);
|
|
}
|
|
|
|
// 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(`
|
|
<html>
|
|
<head><title>Error</title></head>
|
|
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
<h1>Error</h1>
|
|
<p>An error occurred while processing your invite.</p>
|
|
<a href="${SITE_URL}/#/login">Go to Login</a>
|
|
</body>
|
|
</html>
|
|
`);
|
|
}
|
|
});
|
|
|
|
// 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;
|