Implement comprehensive report system with detailed viewing and AI enhancements
- Add detailed report viewing with full-screen ReportDetail component for both company and employee reports - Fix company wiki to display onboarding Q&A in card format matching Figma designs - Exclude company owners from employee submission counts (owners contribute to wiki, not employee data) - Fix employee report generation to include company context (wiki + company report + employee answers) - Fix company report generation to use filtered employee submissions only - Add proper error handling for submission data format variations - Update Firebase functions to use gpt-4o model instead of deprecated gpt-4.1 - Fix UI syntax errors and improve report display functionality - Add comprehensive logging for debugging report generation flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,25 +24,55 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: check for persisted session
|
||||
console.log('Demo mode: checking for persisted session');
|
||||
const sessionUser = sessionStorage.getItem('auditly_demo_session');
|
||||
|
||||
if (isFirebaseConfigured) {
|
||||
// Firebase mode: Set up proper Firebase auth state listener
|
||||
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
||||
console.log('Firebase auth state changed:', firebaseUser?.email);
|
||||
if (firebaseUser) {
|
||||
setUser(firebaseUser);
|
||||
} else {
|
||||
// Check for OTP session as fallback
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring OTP session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
} else {
|
||||
// Demo/OTP mode: Check localStorage for persisted session
|
||||
console.log('Checking for persisted OTP session');
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring demo session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
return () => { };
|
||||
}
|
||||
console.log('Setting up Firebase auth listener');
|
||||
const unsub = onAuthStateChanged(auth, (u) => {
|
||||
console.log('Auth state changed:', u);
|
||||
setUser(u);
|
||||
setLoading(false);
|
||||
});
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
@@ -54,13 +84,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
};
|
||||
|
||||
const signOutUser = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Clear demo session
|
||||
sessionStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
return;
|
||||
try {
|
||||
// Sign out from Firebase if configured and user is signed in via Firebase
|
||||
if (isFirebaseConfigured && auth.currentUser) {
|
||||
await signOut(auth);
|
||||
console.log('Firebase signout completed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Firebase signout error:', error);
|
||||
}
|
||||
await signOut(auth);
|
||||
|
||||
// Always clear all local session data
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
localStorage.removeItem('auditly_auth_token');
|
||||
localStorage.removeItem('auditly_selected_org');
|
||||
sessionStorage.clear();
|
||||
|
||||
setUser(null);
|
||||
console.log('User signed out and all sessions cleared');
|
||||
};
|
||||
|
||||
const signInWithEmail = async (email: string, password: string) => {
|
||||
@@ -79,7 +120,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
console.log('Demo login successful for:', email);
|
||||
} else {
|
||||
throw new Error('Invalid password');
|
||||
@@ -132,7 +173,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
console.log('Demo signup successful for:', email);
|
||||
return;
|
||||
}
|
||||
@@ -191,8 +232,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
sessionStorage.setItem('auditly_auth_token', data.token);
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_auth_token', data.token);
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -206,8 +247,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
sessionStorage.setItem('auditly_auth_token', token);
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_auth_token', token);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,8 +3,9 @@ import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/f
|
||||
import { db, isFirebaseConfigured } from '../services/firebase';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { Employee, Report, Submission, CompanyReport } from '../types';
|
||||
import { REPORT_DATA, SUBMISSIONS_DATA, SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||
import { demoStorage } from '../services/demoStorage';
|
||||
import { apiPost, apiPut } from '../services/api';
|
||||
|
||||
interface OrgData {
|
||||
orgId: string;
|
||||
@@ -98,65 +99,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
// Initialize with empty employee list for clean start
|
||||
// (Removed automatic seeding of 6 default employees per user feedback)
|
||||
|
||||
// Create sample submissions for multiple employees
|
||||
const sampleSubmissions = [
|
||||
{
|
||||
employeeId: 'AG',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
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."
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeId: 'MB',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
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."
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeId: 'KT',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
answers: {
|
||||
role_clarity: "My role as Marketing Manager is clear - I oversee campaigns, analyze performance metrics, and coordinate with sales.",
|
||||
key_outputs: "Launched 5 successful campaigns this quarter, increased lead quality by 30%, improved attribution tracking.",
|
||||
bottlenecks: "Limited budget for premium tools, sometimes slow approval process for creative assets.",
|
||||
hidden_talent: "I have experience with data science and could help build predictive models for customer behavior.",
|
||||
retention_risk: "Overall happy, but would like more strategic input in product positioning and pricing decisions.",
|
||||
energy_distribution: "40% campaign execution, 30% analysis and reporting, 20% strategy, 10% team coordination.",
|
||||
performance_indicators: "Campaign ROI improved by 25%, lead conversion rates increased, better cross-team collaboration.",
|
||||
workflow: "Weekly campaign planning, daily performance monitoring, bi-weekly strategy reviews, monthly board reporting."
|
||||
}
|
||||
}
|
||||
];
|
||||
// Don't automatically create sample submissions - let users create real data
|
||||
// through the proper questionnaire flow
|
||||
|
||||
// Save all sample submissions
|
||||
sampleSubmissions.forEach(submission => {
|
||||
demoStorage.saveSubmission(submission);
|
||||
});
|
||||
// Note: Sample employee reports removed - real reports generated via AI after questionnaire submission
|
||||
|
||||
// Save sample employee report (only for AG initially)
|
||||
demoStorage.saveEmployeeReport(orgId, REPORT_DATA.employeeId, REPORT_DATA);
|
||||
|
||||
// Save sample company report
|
||||
demoStorage.saveCompanyReport(orgId, SAMPLE_COMPANY_REPORT);
|
||||
// Don't save sample company report - let users generate real AI-powered reports
|
||||
}
|
||||
|
||||
// Load persistent demo data
|
||||
@@ -175,7 +123,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
}));
|
||||
setEmployees(convertedEmployees);
|
||||
|
||||
// Convert submissions to expected format
|
||||
// Load any existing submissions from localStorage
|
||||
const orgSubmissions = demoStorage.getSubmissionsByOrg(orgId);
|
||||
const convertedSubmissions: Record<string, Submission> = {};
|
||||
Object.entries(orgSubmissions).forEach(([employeeId, demoSub]) => {
|
||||
@@ -189,11 +137,11 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
});
|
||||
setSubmissions(convertedSubmissions);
|
||||
|
||||
// Convert reports to expected format
|
||||
// Load any existing AI-generated reports from localStorage
|
||||
const orgReports = demoStorage.getEmployeeReportsByOrg(orgId);
|
||||
setReports(orgReports);
|
||||
|
||||
// Get company reports
|
||||
// Load any existing company reports from localStorage
|
||||
const companyReports = demoStorage.getCompanyReportsByOrg(orgId);
|
||||
setFullCompanyReports(companyReports);
|
||||
return;
|
||||
@@ -239,20 +187,29 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
const updatedOrg = { ...(org || { orgId, name: 'Demo Company' }), ...data } as OrgData;
|
||||
setOrg(updatedOrg);
|
||||
|
||||
// Also sync with server for multi-tenant persistence
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/organizations/${orgId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
// If onboarding was completed, update localStorage for persistence and notify other contexts
|
||||
if (data.onboardingCompleted) {
|
||||
const demoOrgData = {
|
||||
orgId: updatedOrg.orgId,
|
||||
name: updatedOrg.name,
|
||||
onboardingCompleted: updatedOrg.onboardingCompleted || false,
|
||||
...updatedOrg // Include all additional fields
|
||||
};
|
||||
demoStorage.saveOrganization(demoOrgData);
|
||||
|
||||
console.log('OrgContext: Onboarding completed, dispatching update event', {
|
||||
orgId: updatedOrg.orgId,
|
||||
onboardingCompleted: true
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to sync organization data with server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to sync organization data:', error);
|
||||
// Signal to UserOrganizationsContext and other components about completion
|
||||
window.dispatchEvent(new CustomEvent('organizationUpdated', {
|
||||
detail: { orgId: updatedOrg.orgId, onboardingCompleted: true }
|
||||
}));
|
||||
}
|
||||
|
||||
// Organization already exists, no need to sync with server during onboarding
|
||||
// We'll update Firestore directly in the Firebase mode below
|
||||
} else {
|
||||
// Firebase mode - save to Firestore
|
||||
const orgRef = doc(db, 'orgs', orgId);
|
||||
@@ -261,6 +218,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
// 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 }
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -302,39 +271,57 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
};
|
||||
|
||||
const inviteEmployee = async ({ name, email }: { name: string; email: string }) => {
|
||||
// Always use Cloud Functions for invites to ensure multi-tenant compliance
|
||||
const response = await fetch(`${API_URL}/createInvitation`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, orgId })
|
||||
});
|
||||
console.log('inviteEmployee called:', { name, email, orgId });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create invite: ${response.status}`);
|
||||
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 data = await response.json();
|
||||
const { code, employee, inviteLink } = data;
|
||||
|
||||
// Store employee locally for immediate UI update
|
||||
if (!isFirebaseConfigured) {
|
||||
const newEmployee = { ...employee, orgId };
|
||||
setEmployees(prev => {
|
||||
if (prev.find(e => e.id === employee.id)) return prev;
|
||||
return [...prev, newEmployee];
|
||||
});
|
||||
demoStorage.saveEmployee(newEmployee);
|
||||
} else {
|
||||
// For Firebase, the employee will be created when they accept the invite
|
||||
// But we can add them to local state for immediate UI update
|
||||
const newEmployee = { ...employee, orgId };
|
||||
setEmployees(prev => {
|
||||
if (prev.find(e => e.id === employee.id)) return prev;
|
||||
return [...prev, newEmployee];
|
||||
});
|
||||
}
|
||||
|
||||
return { employeeId: employee.id, inviteLink };
|
||||
};
|
||||
|
||||
const getReportVersions = async (employeeId: string) => {
|
||||
@@ -407,27 +394,36 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
|
||||
const seedInitialData = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Start with empty employee list for clean demo experience
|
||||
// Start with completely clean slate - no sample data
|
||||
setEmployees([]);
|
||||
setSubmissions({ [SUBMISSIONS_DATA.employeeId]: SUBMISSIONS_DATA });
|
||||
setReports({ [REPORT_DATA.employeeId]: REPORT_DATA });
|
||||
setFullCompanyReports([SAMPLE_COMPANY_REPORT]);
|
||||
setSubmissions({});
|
||||
setReports({});
|
||||
setFullCompanyReports([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with clean slate - let users invite their own employees
|
||||
// (Removed automatic seeding per user feedback)
|
||||
// Start with clean slate - let users invite their own employees and generate real data
|
||||
};
|
||||
|
||||
const saveFullCompanyReport = async (report: CompanyReport) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
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]);
|
||||
// Persist to localStorage
|
||||
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<CompanyReport[]> => {
|
||||
@@ -444,91 +440,132 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
};
|
||||
|
||||
const generateCompanyReport = async (): Promise<CompanyReport> => {
|
||||
// Generate comprehensive company report based on current data
|
||||
const totalEmployees = employees.length;
|
||||
const submittedEmployees = Object.keys(submissions).length;
|
||||
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
|
||||
// Department breakdown (concrete data) - exclude owners
|
||||
const deptMap = new Map<string, number>();
|
||||
employees.forEach(emp => {
|
||||
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 }));
|
||||
|
||||
// Analyze employee reports for insights
|
||||
const reportValues = Object.values(reports) as Report[];
|
||||
const organizationalStrengths: string[] = [];
|
||||
const organizationalRisks: string[] = [];
|
||||
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);
|
||||
|
||||
reportValues.forEach(report => {
|
||||
if (report.strengths) {
|
||||
organizationalStrengths.push(...report.strengths);
|
||||
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');
|
||||
}
|
||||
if (report.risks) {
|
||||
organizationalRisks.push(...report.risks);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove duplicates and take top items
|
||||
const uniqueStrengths = [...new Set(organizationalStrengths)].slice(0, 5);
|
||||
const uniqueRisks = [...new Set(organizationalRisks)].slice(0, 5);
|
||||
const data = await res.json();
|
||||
console.log('Company insights generated via AI successfully');
|
||||
console.log('AI response data:', data);
|
||||
|
||||
const gradingBreakdown = [
|
||||
{ category: 'Execution', value: 70 + Math.random() * 15 },
|
||||
{ category: 'People', value: 70 + Math.random() * 15 },
|
||||
{ category: 'Strategy', value: 65 + Math.random() * 15 },
|
||||
{ category: 'Risk', value: 60 + Math.random() * 15 }
|
||||
];
|
||||
const legacy = gradingBreakdown.reduce<Record<string, number>>((acc, g) => { acc[g.category.toLowerCase()] = Math.round((g.value / 100) * 5 * 10) / 10; return acc; }, {});
|
||||
const report: CompanyReport = {
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
overview: {
|
||||
totalEmployees,
|
||||
departmentBreakdown,
|
||||
submissionRate,
|
||||
lastUpdated: Date.now(),
|
||||
averagePerformanceScore: gradingBreakdown.reduce((a, g) => a + g.value, 0) / gradingBreakdown.length / 20,
|
||||
riskLevel: uniqueRisks.length > 4 ? 'High' : uniqueRisks.length > 2 ? 'Medium' : 'Low'
|
||||
},
|
||||
personnelChanges: { newHires: [], promotions: [], departures: [] },
|
||||
immediateHiringNeeds: [],
|
||||
operatingPlan: {
|
||||
nextQuarterGoals: ['Increase productivity', 'Implement review system'],
|
||||
keyInitiatives: ['Mentorship program'],
|
||||
resourceNeeds: ['Senior engineer'],
|
||||
riskMitigation: ['Cross-training']
|
||||
},
|
||||
forwardOperatingPlan: { // legacy fields
|
||||
quarterlyGoals: ['Increase productivity'],
|
||||
resourceNeeds: ['Senior engineer'],
|
||||
riskMitigation: ['Cross-training']
|
||||
},
|
||||
organizationalStrengths: uniqueStrengths.map(s => ({ area: s, description: s })),
|
||||
organizationalRisks: uniqueRisks,
|
||||
organizationalImpactSummary: 'Impact summary placeholder',
|
||||
gradingBreakdown,
|
||||
gradingOverview: legacy,
|
||||
executiveSummary: `Company overview for ${org?.name || 'Organization'} as of ${new Date().toLocaleDateString()}. Total workforce: ${totalEmployees}. Submission rate: ${submissionRate.toFixed(1)}%. Key strengths: ${uniqueStrengths.slice(0, 2).join(', ')}. Risks: ${uniqueRisks.slice(0, 2).join(', ')}.`
|
||||
};
|
||||
// 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'
|
||||
}
|
||||
};
|
||||
|
||||
await saveFullCompanyReport(report);
|
||||
return report;
|
||||
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<CompanyReport> => {
|
||||
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 {
|
||||
const res = await fetch(`${API_URL}/generateCompanyWiki`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ org: orgData, submissions })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to generate company wiki');
|
||||
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();
|
||||
const data: CompanyReport = payload.report || payload; // backward compatibility
|
||||
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) {
|
||||
@@ -605,12 +642,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
isOwner,
|
||||
issueInviteViaApi: async ({ name, email, role, department }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/createInvitation`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, role, department, orgId })
|
||||
});
|
||||
if (!res.ok) throw new Error('invite creation failed');
|
||||
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 }]);
|
||||
@@ -685,19 +728,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
// Save to localStorage for persistence
|
||||
demoStorage.saveSubmission(submission);
|
||||
|
||||
// Also call Cloud Function for processing with orgId
|
||||
// Also call Cloud Function for processing with authentication and orgId
|
||||
const employee = employees.find(e => e.id === employeeId);
|
||||
const res = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
employeeId,
|
||||
answers,
|
||||
orgId,
|
||||
employee
|
||||
})
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to submit to server');
|
||||
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 = {
|
||||
@@ -727,22 +769,76 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
},
|
||||
generateEmployeeReport: async (employee: Employee) => {
|
||||
try {
|
||||
const submission = submissions[employee.id]?.answers || submissions[employee.id] || {};
|
||||
const res = await fetch(`${API_URL}/generateEmployeeReport`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ employee, submission })
|
||||
});
|
||||
if (!res.ok) throw new Error('failed to generate');
|
||||
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<string, string> = {};
|
||||
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<string, string>);
|
||||
} else {
|
||||
// If answers is already a key-value object
|
||||
submissionAnswers = submission.answers as Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getEmployeeReport: async (employeeId: string) => {
|
||||
try {
|
||||
|
||||
@@ -70,9 +70,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize selected org from session storage
|
||||
// Initialize selected org from localStorage (persistent across sessions)
|
||||
useEffect(() => {
|
||||
const savedOrgId = sessionStorage.getItem('auditly_selected_org');
|
||||
const savedOrgId = localStorage.getItem('auditly_selected_org');
|
||||
if (savedOrgId) {
|
||||
setSelectedOrgId(savedOrgId);
|
||||
}
|
||||
@@ -83,9 +83,48 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
||||
loadOrganizations();
|
||||
}, [user]);
|
||||
|
||||
// Listen for organization updates (e.g., onboarding completion)
|
||||
useEffect(() => {
|
||||
const handleOrgUpdate = (event: CustomEvent) => {
|
||||
const { orgId, onboardingCompleted } = event.detail;
|
||||
console.log('UserOrganizationsContext received org update:', { orgId, onboardingCompleted });
|
||||
|
||||
if (onboardingCompleted && orgId) {
|
||||
// Update the specific organization in the list to reflect onboarding completion
|
||||
setOrganizations(prev => {
|
||||
const updated = prev.map(org =>
|
||||
org.orgId === orgId
|
||||
? { ...org, onboardingCompleted: true }
|
||||
: org
|
||||
);
|
||||
console.log('Updated organizations after onboarding completion:', updated);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('organizationUpdated', handleOrgUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('organizationUpdated', handleOrgUpdate as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const selectOrganization = (orgId: string) => {
|
||||
console.log('Switching to organization:', orgId);
|
||||
|
||||
// Clear any cached data when switching organizations for security
|
||||
sessionStorage.removeItem('auditly_cached_employees');
|
||||
sessionStorage.removeItem('auditly_cached_submissions');
|
||||
sessionStorage.removeItem('auditly_cached_reports');
|
||||
|
||||
setSelectedOrgId(orgId);
|
||||
sessionStorage.setItem('auditly_selected_org', orgId);
|
||||
localStorage.setItem('auditly_selected_org', orgId);
|
||||
|
||||
// Dispatch event to notify other contexts about the org switch
|
||||
window.dispatchEvent(new CustomEvent('organizationChanged', {
|
||||
detail: { newOrgId: orgId }
|
||||
}));
|
||||
};
|
||||
|
||||
const createOrganization = async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => {
|
||||
|
||||
Reference in New Issue
Block a user