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:
Ra
2025-08-18 19:08:29 -07:00
parent 557b113196
commit 1a9e92d7bd
20 changed files with 1793 additions and 635 deletions

View File

@@ -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 (

View File

@@ -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 {

View File

@@ -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 }> => {