import React, { createContext, useContext, useEffect, useState } from 'react'; import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/firestore'; import { db, isFirebaseConfigured } from '../services/firebase'; import { useAuth } from './AuthContext'; import { Employee, Report, Submission, CompanyReport } from '../types'; import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants'; import { demoStorage } from '../services/demoStorage'; import { apiPost, apiPut } from '../services/api'; interface OrgData { orgId: string; name: string; industry?: string; size?: string; description?: string; mission?: string; vision?: string; values?: string; foundingYear?: string; evolution?: string; majorMilestones?: string; advantages?: string; vulnerabilities?: string; competitors?: string; marketPosition?: string; currentChallenges?: string; shortTermGoals?: string; longTermGoals?: string; keyMetrics?: string; cultureDescription?: string; workEnvironment?: string; leadershipStyle?: string; communicationStyle?: string; additionalContext?: string; onboardingCompleted?: boolean; } interface OrgContextType { org: OrgData | null; orgId: string; employees: Employee[]; submissions: Record; reports: Record; upsertOrg: (data: Partial) => Promise; saveReport: (employeeId: string, report: Report) => Promise; inviteEmployee: (args: { name: string; email: string }) => Promise<{ employeeId: string; inviteLink: string }>; issueInviteViaApi: (args: { name: string; email: string; role?: string; department?: string }) => Promise<{ code: string; inviteLink: string; emailLink: string; employee: any }>; getInviteStatus: (code: string) => Promise<{ used: boolean; employee: any } | null>; consumeInvite: (code: string) => Promise<{ employee: any } | null>; getReportVersions: (employeeId: string) => Promise>; saveReportVersion: (employeeId: string, report: Report) => Promise; acceptInvite: (code: string) => Promise; saveCompanyReport: (summary: string) => Promise; getCompanyReportHistory: () => Promise>; saveFullCompanyReport: (report: CompanyReport) => Promise; getFullCompanyReportHistory: () => Promise; generateCompanyReport: () => Promise; generateCompanyWiki: (orgOverride?: OrgData) => Promise; seedInitialData: () => Promise; isOwner: (employeeId?: string) => boolean; submitEmployeeAnswers: (employeeId: string, answers: Record) => Promise; generateEmployeeReport: (employee: Employee) => Promise; getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: Report; error?: string }>; getEmployeeReports: () => Promise<{ success: boolean; reports?: Report[]; error?: string }>; } const OrgContext = createContext(undefined); export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: string }> = ({ children, selectedOrgId }) => { const { user } = useAuth(); const [org, setOrg] = useState(null); const [employees, setEmployees] = useState([]); const [submissions, setSubmissions] = useState>({}); const [reports, setReports] = useState>({}); const [reportVersions, setReportVersions] = useState>>({}); const [companyReports, setCompanyReports] = useState>([]); const [fullCompanyReports, setFullCompanyReports] = useState([]); // Use the provided selectedOrgId instead of deriving from user const orgId = selectedOrgId; useEffect(() => { console.log('OrgContext effect running, orgId:', orgId, 'isFirebaseConfigured:', isFirebaseConfigured); if (!orgId) return; // Wait for orgId to be available console.log('Setting up Firebase org data'); const orgRef = doc(db, 'orgs', orgId); getDoc(orgRef).then(async (snap) => { if (snap.exists()) { setOrg({ orgId, ...(snap.data() as any) }); } else { const seed = { name: 'Your Company', onboardingCompleted: false }; await setDoc(orgRef, seed); setOrg({ orgId, ...(seed as any) }); } }); const employeesCol = collection(db, 'orgs', orgId, 'employees'); const unsubEmp = onSnapshot(employeesCol, (snap) => { const arr: Employee[] = []; snap.forEach((d) => arr.push({ id: d.id, ...(d.data() as any) })); setEmployees(arr); }); const submissionsCol = collection(db, 'orgs', orgId, 'submissions'); const unsubSub = onSnapshot(submissionsCol, (snap) => { const map: Record = {}; snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) })); setSubmissions(map); }); const reportsCol = collection(db, 'orgs', orgId, 'reports'); const unsubRep = onSnapshot(reportsCol, (snap) => { const map: Record = {}; snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) } as Report)); setReports(map); }); return () => { unsubEmp(); unsubSub(); unsubRep(); }; }, [orgId]); const upsertOrg = async (data: Partial) => { const orgRef = doc(db, 'orgs', orgId); await setDoc(orgRef, data, { merge: true }); // Update local state const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData; setOrg(updatedOrg); // If onboarding was completed, notify other contexts if (data.onboardingCompleted) { console.log('OrgContext (Firebase): Onboarding completed, dispatching update event', { orgId: updatedOrg.orgId, onboardingCompleted: true }); window.dispatchEvent(new CustomEvent('organizationUpdated', { detail: { orgId: updatedOrg.orgId, onboardingCompleted: true } })); } }; const saveReport = async (employeeId: string, report: Report) => { const ref = doc(db, 'orgs', orgId, 'reports', employeeId); await setDoc(ref, report, { merge: true }); }; const inviteEmployee = async ({ name, email }: { name: string; email: string }) => { console.log('inviteEmployee called:', { name, email, orgId }); try { // Always use Cloud Functions for invites to ensure multi-tenant compliance const res = await apiPost('/createInvitation', { name, email }, orgId); if (!res.ok) { const errorData = await res.json(); console.error('Invite creation failed:', errorData); throw new Error(errorData.error || `Failed to create invite: ${res.status}`); } const data = await res.json(); const { code, employee, inviteLink } = data; console.log('Invite created successfully:', { code, employee: employee.name, inviteLink }); // Store employee locally for immediate UI update with proper typing const newEmployee: Employee = { id: employee.id, name: employee.name, email: employee.email, initials: employee.name ? employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : employee.email.substring(0, 2).toUpperCase(), department: employee.department, role: employee.role, isOwner: false }; if (!isFirebaseConfigured) { const employeeWithOrg = { ...newEmployee, orgId }; setEmployees(prev => { if (prev.find(e => e.id === employee.id)) return prev; return [...prev, newEmployee]; }); demoStorage.saveEmployee(employeeWithOrg); } else { // For Firebase, add to local state for immediate UI update setEmployees(prev => { if (prev.find(e => e.id === employee.id)) return prev; return [...prev, newEmployee]; }); } return { employeeId: employee.id, inviteLink }; } catch (error) { console.error('inviteEmployee error:', error); throw error; } }; const getReportVersions = async (employeeId: string) => { if (!isFirebaseConfigured) { return reportVersions[employeeId] || []; } const col = collection(db, 'orgs', orgId, 'reports', employeeId, 'versions'); const snap = await getDocs(col); const arr: Array<{ id: string; createdAt: number; report: Report }> = []; snap.forEach(d => { const data = d.data() as any; arr.push({ id: d.id, createdAt: data.createdAt ?? 0, report: data.report as Report }); }); return arr.sort((a, b) => b.createdAt - a.createdAt); }; const saveReportVersion = async (employeeId: string, report: Report) => { const version = { id: Date.now().toString(), createdAt: Date.now(), report }; if (!isFirebaseConfigured) { setReportVersions(prev => ({ ...prev, [employeeId]: [version, ...(prev[employeeId] || [])] })); return; } const ref = doc(db, 'orgs', orgId, 'reports', employeeId, 'versions', version.id); await setDoc(ref, { createdAt: version.createdAt, report }); }; const acceptInvite = async (code: string) => { if (!code) return; if (!isFirebaseConfigured) { // Demo mode: mark invite as used demoStorage.markInviteUsed(code); return; } const inviteRef = doc(db, 'orgs', orgId, 'invites', code); const snap = await getDoc(inviteRef); if (!snap.exists()) return; const data = snap.data() as any; // Minimal: mark accepted await setDoc(inviteRef, { ...data, acceptedAt: Date.now() }, { merge: true }); }; const saveCompanyReport = async (summary: string) => { const id = Date.now().toString(); const createdAt = Date.now(); if (!isFirebaseConfigured) { const reportData = { id, createdAt, summary }; setCompanyReports(prev => [reportData, ...prev]); // Persist to localStorage (note: this method stores simple reports) return; } const ref = doc(db, 'orgs', orgId, 'companyReports', id); await setDoc(ref, { createdAt, summary }); }; const getCompanyReportHistory = async () => { if (!isFirebaseConfigured) { return companyReports; } const col = collection(db, 'orgs', orgId, 'companyReports'); const snap = await getDocs(col); const arr: Array<{ id: string; createdAt: number; summary: string }> = []; snap.forEach(d => { const data = d.data() as any; arr.push({ id: d.id, createdAt: data.createdAt ?? 0, summary: data.summary ?? '' }); }); return arr.sort((a, b) => b.createdAt - a.createdAt); }; const seedInitialData = async () => { if (!isFirebaseConfigured) { // Start with completely clean slate - no sample data setEmployees([]); setSubmissions({}); setReports({}); setFullCompanyReports([]); return; } // Start with clean slate - let users invite their own employees and generate real data }; const saveFullCompanyReport = async (report: CompanyReport) => { if (!orgId) { console.error('Cannot save company report: orgId is undefined'); throw new Error('Organization ID is required to save company report'); } if (!isFirebaseConfigured || !db) { // Fallback to local storage in demo mode setFullCompanyReports(prev => [report, ...prev]); demoStorage.saveCompanyReport(orgId, report); return; } // Use direct Firestore operations - much more efficient const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id); await setDoc(ref, report); // Update local state after successful save setFullCompanyReports(prev => [report, ...prev]); }; const getFullCompanyReportHistory = async (): Promise => { if (!isFirebaseConfigured) { return fullCompanyReports; } const col = collection(db, 'orgs', orgId, 'fullCompanyReports'); const snap = await getDocs(col); const arr: CompanyReport[] = []; snap.forEach(d => { arr.push({ id: d.id, ...d.data() } as CompanyReport); }); return arr.sort((a, b) => b.createdAt - a.createdAt); }; const generateCompanyReport = async (): Promise => { console.log('generateCompanyReport called for org:', orgId); // Calculate concrete metrics from actual data (no AI needed) // Exclude owners from employee counts - they are company wiki contributors, not employees const actualEmployees = employees.filter(emp => !emp.isOwner); const totalEmployees = actualEmployees.length; // Only count submissions from non-owner employees const employeeSubmissions = Object.fromEntries( Object.entries(submissions).filter(([employeeId]) => { const employee = employees.find(emp => emp.id === employeeId); return employee && !employee.isOwner; }) ); const submittedEmployees = Object.keys(employeeSubmissions).length; const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0; // Department breakdown (concrete data) - exclude owners const deptMap = new Map(); actualEmployees.forEach(emp => { const dept = emp.department || 'Unassigned'; deptMap.set(dept, (deptMap.get(dept) || 0) + 1); }); const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count })); try { // Use AI only for analysis and insights that require reasoning const res = await apiPost('/generateCompanyWiki', { org: org, submissions: employeeSubmissions, // Only employee submissions, not owner data metrics: { totalEmployees, submissionRate, departmentBreakdown } }, orgId); if (!res.ok) { const errorData = await res.json(); console.error('Company report generation failed:', errorData); throw new Error(errorData.error || 'Failed to generate company report'); } const data = await res.json(); console.log('Company insights generated via AI successfully'); console.log('AI response data:', data); // Combine concrete metrics with AI insights const report: CompanyReport = { id: Date.now().toString(), createdAt: Date.now(), // Use AI-generated insights for subjective analysis ...data.report, // Override with our concrete metrics overview: { totalEmployees, departmentBreakdown, submissionRate, lastUpdated: Date.now(), averagePerformanceScore: data.report?.overview?.averagePerformanceScore || 0, riskLevel: data.report?.overview?.riskLevel || 'Unknown' } }; console.log('Final company report object:', report); await saveFullCompanyReport(report); return report; } catch (error) { console.error('generateCompanyReport error:', error); throw error; } }; const generateCompanyWiki = async (orgOverride?: OrgData): Promise => { const orgData = orgOverride || org; console.log('generateCompanyWiki called with:', { orgData, orgId, submissionsCount: Object.keys(submissions || {}).length, isFirebaseConfigured }); if (!orgId) { throw new Error('Organization ID is required to generate company wiki'); } // ALWAYS use API call for wiki generation, with local fallback try { console.log('Making API call to generateCompanyWiki...'); const res = await apiPost('/generateCompanyWiki', { org: orgData, submissions: submissions || [] }, orgId); console.log('API response status:', res.status); if (!res.ok) { const errorData = await res.json(); console.error('API error response:', errorData); throw new Error(errorData.error || 'Failed to generate company wiki'); } const payload = await res.json(); console.log('API success response:', payload); // Ensure the report has all required fields to prevent undefined errors const data: CompanyReport = { id: Date.now().toString(), createdAt: Date.now(), overview: { totalEmployees: employees.length, departmentBreakdown: [], submissionRate: 0, lastUpdated: Date.now(), averagePerformanceScore: 0, riskLevel: 'Unknown' }, gradingBreakdown: [], operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] }, personnelChanges: { newHires: [], promotions: [], departures: [] }, keyPersonnelChanges: [], immediateHiringNeeds: [], forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] }, organizationalStrengths: [], organizationalRisks: [], gradingOverview: {}, executiveSummary: 'Company report generated successfully.', // Override with API data if available ...(payload.report || payload) }; await saveFullCompanyReport(data); return data; } catch (e) { console.error('generateCompanyWiki error, falling back to local synthetic:', e); return generateCompanyReport(); } }; const isOwner = (employeeId?: string): boolean => { const currentEmployee = employeeId ? employees.find(e => e.id === employeeId) : employees.find(e => e.email === user?.email); return currentEmployee?.isOwner === true; }; const getEmployeeReport = async (employeeId: string) => { try { if (isFirebaseConfigured && user) { // Firebase implementation const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId)); if (reportDoc.exists()) { return { success: true, report: reportDoc.data() }; } return { success: false, error: 'Report not found' }; } else { // Demo mode - call API const response = await fetch(`${API_URL}/api/employee-report/${employeeId}`); const result = await response.json(); return result; } } catch (error) { console.error('Error fetching employee report:', error); return { success: false, error: error.message }; } }; const getEmployeeReports = async () => { try { if (isFirebaseConfigured && user) { // Firebase implementation const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports')); const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); return { success: true, reports }; } else { // Demo mode - call API const response = await fetch(`${API_URL}/api/employee-reports`); const result = await response.json(); return result; } } catch (error) { console.error('Error fetching employee reports:', error); return { success: false, error: error.message }; } }; const value = { org, orgId, employees, submissions, reports, upsertOrg, saveReport, inviteEmployee, getReportVersions, saveReportVersion, acceptInvite, saveCompanyReport, getCompanyReportHistory, saveFullCompanyReport, getFullCompanyReportHistory, generateCompanyReport, generateCompanyWiki, seedInitialData, isOwner, issueInviteViaApi: async ({ name, email, role, department }) => { try { const res = await apiPost('/createInvitation', { name, email, role, department }, orgId); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || 'Invite creation failed'); } const json = await res.json(); // Optimistically add employee shell (not yet active until consume) setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, { ...json.employee }]); return json; } catch (e) { console.error('issueInviteViaApi error', e); throw e; } }, getInviteStatus: async (code: string) => { if (!isFirebaseConfigured) { // Demo mode: check localStorage first, then server const invite = demoStorage.getInvite(code); if (invite) { return { used: invite.used, employee: invite.employee }; } } try { const res = await fetch(`${API_URL}/getInvitationStatus?code=${code}`); if (!res.ok) return null; return await res.json(); } catch (e) { console.error('getInviteStatus error', e); return null; } }, consumeInvite: async (code: string) => { if (!isFirebaseConfigured) { // Demo mode: mark invite as used in localStorage and update state const invite = demoStorage.getInvite(code); if (invite && !invite.used) { demoStorage.markInviteUsed(code); // Ensure employee is in the list with proper typing const convertedEmployee: Employee = { id: invite.employee.id, name: invite.employee.name, email: invite.employee.email, initials: invite.employee.name ? invite.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : invite.employee.email.substring(0, 2).toUpperCase(), department: invite.employee.department, role: invite.employee.role }; setEmployees(prev => prev.find(e => e.id === invite.employee.id) ? prev : [...prev, convertedEmployee]); return { employee: convertedEmployee }; } return null; } try { const res = await fetch(`${API_URL}/consumeInvitation?code=${code}`, { method: 'POST' }); if (!res.ok) return null; const json = await res.json(); // Mark employee as active (could set a flag later) setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, json.employee]); return json; } catch (e) { console.error('consumeInvite error', e); return null; } }, submitEmployeeAnswers: async (employeeId: string, answers: Record) => { if (!isFirebaseConfigured) { // Demo mode: save to localStorage and call server endpoint try { const submission = { employeeId, orgId, answers, createdAt: Date.now() }; // Save to localStorage for persistence demoStorage.saveSubmission(submission); // Also call Cloud Function for processing with authentication and orgId const employee = employees.find(e => e.id === employeeId); const res = await apiPost('/submitEmployeeAnswers', { employeeId, answers, employee }, orgId); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || 'Failed to submit to server'); } // Update local state for UI with proper typing const convertedSubmission: Submission = { employeeId, answers: Object.entries(answers).map(([question, answer]) => ({ question, answer })) }; setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission })); return true; } catch (e) { console.error('submitEmployeeAnswers error', e); return false; } } // Firebase mode: save to Firestore try { const ref = doc(db, 'orgs', orgId, 'submissions', employeeId); await setDoc(ref, { ...answers, createdAt: Date.now() }, { merge: true }); return true; } catch (e) { console.error('submitEmployeeAnswers error', e); return false; } }, generateEmployeeReport: async (employee: Employee) => { try { console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId); // Get submission data for this employee const submission = submissions[employee.id]; if (!submission) { throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`); } // Convert submission format for API let submissionAnswers: Record = {}; if (submission.answers) { if (Array.isArray(submission.answers)) { // If answers is an array of {question, answer} objects submissionAnswers = submission.answers.reduce((acc, item) => { acc[item.question] = item.answer; return acc; }, {} as Record); } else { // If answers is already a key-value object submissionAnswers = submission.answers as Record; } } console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers'); // Get company report and wiki data for context let companyWiki = null; try { const companyReports = await getFullCompanyReportHistory(); if (companyReports.length > 0) { companyWiki = { org: org, companyReport: companyReports[0] }; console.log('Including company context in employee report generation'); } } catch (error) { console.warn('Could not fetch company report for context:', error); } const res = await apiPost('/generateEmployeeReport', { employee, submission: submissionAnswers, companyWiki }, orgId); if (!res.ok) { const errorData = await res.json(); console.error('API error response:', errorData); throw new Error(errorData.error || 'Failed to generate employee report'); } const json = await res.json(); if (json.report) { console.log('Employee report generated successfully'); setReports(prev => ({ ...prev, [employee.id]: json.report })); // Also save to persistent storage in demo mode if (!isFirebaseConfigured) { demoStorage.saveEmployeeReport(orgId, employee.id, json.report); } return json.report as Report; } else { throw new Error('No report data received from API'); } } catch (e) { console.error('generateEmployeeReport error', e); throw e; // Re-throw to allow caller to handle } }, getEmployeeReport: async (employeeId: string) => { try { if (isFirebaseConfigured && user) { // Firebase implementation const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId)); if (reportDoc.exists()) { return { success: true, report: reportDoc.data() }; } return { success: false, error: 'Report not found' }; } else { // Demo mode - call Cloud Function const response = await fetch(`${API_URL}/generateEmployeeReport?employeeId=${employeeId}`); const result = await response.json(); return result; } } catch (error) { console.error('Error fetching employee report:', error); return { success: false, error: error.message }; } }, getEmployeeReports: async () => { try { if (isFirebaseConfigured && user) { // Firebase implementation const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports')); const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); return { success: true, reports }; } else { // Demo mode - call Cloud Function const response = await fetch(`${API_URL}/generateEmployeeReport`); const result = await response.json(); return result; } } catch (error) { console.error('Error fetching employee reports:', error); return { success: false, error: error.message }; } }, }; return ( {children} ); }; export const useOrg = () => { const ctx = useContext(OrgContext); if (!ctx) throw new Error('useOrg must be used within OrgProvider'); return ctx; };