fix most of the listed bugs

This commit is contained in:
Ra
2025-08-26 11:23:27 -07:00
parent 772d1d4c10
commit ad15aaa35e
22 changed files with 3493 additions and 2375 deletions

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useAuth } from './AuthContext';
import { Employee, EmployeeReport, Submission, CompanyReport } from '../types';
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
@@ -9,11 +9,19 @@ import { secureApi } from '../services/secureApi';
interface OrgData {
orgId: string;
name?: string;
companyName?: string;
onboardingData?: Record<string, any>;
companyLogo?: string;
updatedAt?: number;
onboardingCompleted?: boolean;
ownerId?: string;
ownerInfo?: {
id: string;
name: string;
email: string;
joinedAt: number;
};
}
interface OrgContextType {
@@ -63,7 +71,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Use the provided selectedOrgId instead of deriving from user
const orgId = selectedOrgId;
// Load initial data using secure API
// Load initial data using secure API - memoized to prevent unnecessary re-runs
useEffect(() => {
if (!orgId || !user?.uid) {
setLoading(false);
@@ -76,54 +84,57 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
try {
setLoading(true);
// Load organization data
try {
const orgData = await secureApi.getOrgData();
setOrg({ orgId, ...orgData });
} catch (error) {
console.warn('Could not load org data, creating default:', error);
// Create default org if not found
// Batch all API calls for better performance
const [orgData, employeesData, submissionsData, reportsData, companyReportsData] = await Promise.allSettled([
secureApi.getOrgData().catch(() => null),
secureApi.getEmployees().catch(() => []),
secureApi.getSubmissions().catch(() => ({})),
secureApi.getReports().catch(() => ({})),
secureApi.getCompanyReports().catch(() => [])
]);
// Process organization data
if (orgData.status === 'fulfilled' && orgData.value) {
setOrg({ orgId, ...orgData.value });
} else {
console.warn('Could not load org data, creating default');
const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
await secureApi.updateOrgData(defaultOrg);
setOrg({ orgId, ...defaultOrg });
}
// Load employees
try {
const employeesData = await secureApi.getEmployees();
setEmployees(employeesData.map(emp => ({
// Process employees data
if (employeesData.status === 'fulfilled') {
setEmployees(employeesData.value.map(emp => ({
...emp,
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
})));
} catch (error) {
console.warn('Could not load employees:', error);
} else {
console.warn('Could not load employees');
setEmployees([]);
}
// Load submissions
try {
const submissionsData = await secureApi.getSubmissions();
setSubmissions(submissionsData);
} catch (error) {
console.warn('Could not load submissions:', error);
// Process submissions data
if (submissionsData.status === 'fulfilled') {
setSubmissions(submissionsData.value);
} else {
console.warn('Could not load submissions');
setSubmissions({});
}
// Load reports
try {
const reportsData = await secureApi.getReports();
setReports(reportsData as Record<string, EmployeeReport>);
} catch (error) {
console.warn('Could not load reports:', error);
// Process reports data
if (reportsData.status === 'fulfilled') {
setReports(reportsData.value as Record<string, EmployeeReport>);
} else {
console.warn('Could not load reports');
setReports({});
}
// Load company reports
try {
const companyReportsData = await secureApi.getCompanyReports();
setFullCompanyReports(companyReportsData);
} catch (error) {
console.warn('Could not load company reports:', error);
// Process company reports data
if (companyReportsData.status === 'fulfilled') {
setFullCompanyReports(companyReportsData.value);
} else {
console.warn('Could not load company reports');
setFullCompanyReports([]);
}
@@ -135,9 +146,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
};
loadOrgData();
}, [orgId, user?.uid]);
}, [orgId, user?.uid]); // Only re-run when orgId or user changes
const upsertOrg = async (data: Partial<OrgData>) => {
const upsertOrg = useCallback(async (data: Partial<OrgData>) => {
if (!user?.uid) {
throw new Error('User authentication required');
}
@@ -164,9 +175,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('Failed to update organization:', error);
throw error;
}
};
}, [user?.uid, org, orgId]);
const saveReport = async (employeeId: string, report: EmployeeReport) => {
const saveReport = useCallback(async (employeeId: string, report: EmployeeReport) => {
if (!user?.uid) {
throw new Error('User authentication required');
}
@@ -180,7 +191,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('Failed to save report:', error);
throw error;
}
};
}, [user?.uid]);
const inviteEmployee = async ({ name, email, role, department }: { name: string; email: string, role?: string, department?: string }) => {
console.log('inviteEmployee called:', { name, email, orgId });
@@ -199,7 +210,6 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
initials: data.employee.name ? data.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase(),
department: data.employee.department,
role: data.employee.role,
isOwner: false,
status: data.employee.status
};
@@ -332,17 +342,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}
// 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);
// Employees collection only contains actual employees (owners are not in this collection)
const actualEmployees = employees;
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;
})
);
// Count submissions from employees
const employeeSubmissions = submissions;
const submittedEmployees = Object.keys(employeeSubmissions).length;
const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
@@ -356,7 +361,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
try {
// Use secure API for AI generation
const data = await secureApi.generateCompanyWiki({
let response = await secureApi.generateCompanyWiki({
...org,
metrics: {
totalEmployees,
@@ -368,25 +373,25 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.log('Company insights generated via AI successfully');
// Combine concrete metrics with AI insights
const report: CompanyReport = {
let report: CompanyReport = {
id: Date.now().toString(),
createdAt: Date.now(),
// Use AI-generated insights for subjective analysis
...(data as any),
// Override with our concrete metrics
overview: {
totalEmployees,
departmentBreakdown,
submissionRate,
lastUpdated: Date.now(),
averagePerformanceScore: (data as any)?.overview?.averagePerformanceScore || 0,
riskLevel: (data as any)?.overview?.riskLevel || 'Unknown'
}
averagePerformanceScore: (response as any)?.overview?.averagePerformanceScore || 0,
riskLevel: (response as any)?.overview?.riskLevel || 'Unknown'
},
...(response as any)
};
console.log('Final company report object:', report);
await saveFullCompanyReport(report);
return report;
// await saveFullCompanyReport(report);
return response;
} catch (error) {
console.error('generateCompanyReport error:', error);
throw error;
@@ -408,12 +413,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Use secure API for wiki generation
try {
console.log('Making API call to generateCompanyWiki...');
const payload = await secureApi.generateCompanyWiki(orgData, Object.values(submissions || {}));
let response = await secureApi.generateCompanyWiki(orgData, Object.values(submissions || {}));
console.log('API success response:', payload);
console.log('API success response:', response);
// Ensure the report has all required fields to prevent undefined errors
const data: CompanyReport = {
const report: CompanyReport = {
id: Date.now().toString(),
createdAt: Date.now(),
overview: {
@@ -430,21 +435,22 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
executiveSummary: 'Company report generated successfully.',
// Override with API data if available
...(payload as any || {})
...(response as any || {})
};
await saveFullCompanyReport(data);
return data;
// await saveFullCompanyReport(data);
return response;
} 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 isOwner = (userId?: string): boolean => {
// Check if the given user ID matches the org owner ID
// If no userId provided, check current user
const targetUserId = userId || user?.uid;
return targetUserId === org?.ownerId;
};
const getEmployeeReport = async (employeeId: string) => {
@@ -483,7 +489,141 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}
};
const value = {
// Memoize functions that don't need dependencies
const issueInviteViaApi = useCallback(async ({ name, email, role, department }) => {
try {
if (!user?.uid) {
throw new Error('User authentication required');
}
const data = await secureApi.createInvitation({ name, email, role, department });
// Optimistically add employee shell (not yet active until consume)
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
...data.employee,
initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
} as Employee]);
return data;
} catch (e) {
console.error('issueInviteViaApi error', e);
throw e;
}
}, [user?.uid]);
const getInviteStatus = useCallback(async (code: string) => {
try {
return await secureApi.getInvitationStatus(code);
} catch (e) {
console.error('getInviteStatus error', e);
return null;
}
}, []);
const consumeInvite = useCallback(async (code: string) => {
try {
if (!user?.uid) {
throw new Error('User authentication required');
}
const result = await secureApi.consumeInvitation(code, user.uid);
// Mark employee as active
if (result && (result as any).employee) {
setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
return { ...(result as any), orgId: org?.orgId };
}
return null;
} catch (e) {
console.error('consumeInvite error', e);
return null;
}
}, [user?.uid, org?.orgId]);
const submitEmployeeAnswers = useCallback(async (employeeId: string, answers: Record<string, string>) => {
try {
// Use secure API for submission
await secureApi.submitEmployeeAnswers(employeeId, answers);
// Update local state for immediate UI feedback
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;
}
}, []);
const generateEmployeeReport = useCallback(async (employee: Employee) => {
try {
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
if (!user?.uid) {
throw new Error('User authentication required');
}
// 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: any) => {
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 data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
if ((data as any).report) {
console.log('Employee report generated successfully');
const report = (data as any).report as EmployeeReport;
setReports(prev => ({ ...prev, [employee.id]: report }));
return 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
}
}, [user?.uid, orgId, submissions, org, getFullCompanyReportHistory]);
// Memoize the entire context value to prevent unnecessary re-renders
const value = useMemo(() => ({
org,
orgId,
employees,
@@ -504,136 +644,42 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
generateCompanyWiki,
seedInitialData,
isOwner,
issueInviteViaApi: async ({ name, email, role, department }) => {
try {
if (!user?.uid) {
throw new Error('User authentication required');
}
const data = await secureApi.createInvitation({ name, email, role, department });
// Optimistically add employee shell (not yet active until consume)
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
...data.employee,
initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
} as Employee]);
return data;
} catch (e) {
console.error('issueInviteViaApi error', e);
throw e;
}
},
getInviteStatus: async (code: string) => {
try {
return await secureApi.getInvitationStatus(code);
} catch (e) {
console.error('getInviteStatus error', e);
return null;
}
},
consumeInvite: async (code: string) => {
try {
if (!user?.uid) {
throw new Error('User authentication required');
}
const result = await secureApi.consumeInvitation(code, user.uid);
// Mark employee as active
if (result && (result as any).employee) {
setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
return { ...(result as any), orgId: org?.orgId };
}
return null;
} catch (e) {
console.error('consumeInvite error', e);
return null;
}
},
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
try {
// Use secure API for submission
await secureApi.submitEmployeeAnswers(employeeId, answers);
// Update local state for immediate UI feedback
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;
}
},
generateEmployeeReport: async (employee: Employee) => {
try {
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
if (!user?.uid) {
throw new Error('User authentication required');
}
// 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: any) => {
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 data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
if ((data as any).report) {
console.log('Employee report generated successfully');
const report = (data as any).report as EmployeeReport;
setReports(prev => ({ ...prev, [employee.id]: report }));
return 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
}
},
issueInviteViaApi,
getInviteStatus,
consumeInvite,
submitEmployeeAnswers,
generateEmployeeReport,
getEmployeeReport,
getEmployeeReports,
};
}), [
org,
orgId,
employees,
submissions,
reports,
loading,
upsertOrg,
saveReport,
inviteEmployee,
getReportVersions,
saveReportVersion,
acceptInvite,
saveCompanyReport,
getCompanyReportHistory,
saveFullCompanyReport,
getFullCompanyReportHistory,
generateCompanyReport,
generateCompanyWiki,
seedInitialData,
isOwner,
issueInviteViaApi,
getInviteStatus,
consumeInvite,
submitEmployeeAnswers,
generateEmployeeReport,
getEmployeeReport,
getEmployeeReports,
]);
return (
<OrgContext.Provider value={value}>