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

@@ -2,9 +2,10 @@ import React, { useState, useMemo } from 'react';
import { Card, Button } from '../components/UiKit';
import { useOrg } from '../contexts/OrgContext';
import { CHAT_STARTERS } from '../constants';
import { apiPost } from '../services/api';
const Chat: React.FC = () => {
const { employees, reports, generateEmployeeReport } = useOrg();
const { employees, reports, generateEmployeeReport, orgId } = useOrg();
const [messages, setMessages] = useState<Array<{ id: string, role: 'user' | 'assistant', text: string }>>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -34,16 +35,44 @@ const Chat: React.FC = () => {
setInput('');
setIsLoading(true);
// Simulate AI response (placeholder for server /api/chat usage)
setTimeout(() => {
try {
// Build context for the AI
const context = {
selectedEmployee: selectedEmployeeId ? employees.find(e => e.id === selectedEmployeeId) : null,
selectedReport: selectedReport,
totalEmployees: employees.length,
organizationScope: !selectedEmployeeId
};
const res = await apiPost('/chat', {
message: textToSend,
employeeId: selectedEmployeeId,
context
}, orgId);
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || 'Failed to get AI response');
}
const data = await res.json();
const aiResponse = {
id: (Date.now() + 1).toString(),
role: 'assistant' as const,
text: `Based on ${selectedEmployeeId ? 'the selected employee\'s' : 'organizational'} data, here's an insight related to: "${textToSend}".`
text: data.response || 'I apologize, but I couldn\'t generate a response at this time.'
};
setMessages(prev => [...prev, aiResponse]);
} catch (error) {
console.error('Chat error:', error);
const errorResponse = {
id: (Date.now() + 1).toString(),
role: 'assistant' as const,
text: `I apologize, but I encountered an error: ${error.message}. Please try again.`
};
setMessages(prev => [...prev, errorResponse]);
} finally {
setIsLoading(false);
}, 1500);
}
};
return (

View File

@@ -113,19 +113,19 @@ const CompanyWiki: React.FC = () => {
<div className="text-sm text-[--text-secondary]">Employees</div>
</div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-green-500">{companyReport.overview.departmentBreakdown.length}</div>
<div className="text-2xl font-bold text-green-500">{companyReport.overview?.departmentBreakdown?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Departments</div>
</div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-purple-500">{companyReport.organizationalStrengths.length}</div>
<div className="text-2xl font-bold text-purple-500">{companyReport.organizationalStrengths?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Strength Areas</div>
</div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-orange-500">{companyReport.organizationalRisks.length}</div>
<div className="text-2xl font-bold text-orange-500">{companyReport.organizationalRisks?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Risks</div>
</div>
</div>
{companyReport.gradingOverview && (
{(Array.isArray(companyReport.gradingOverview) && companyReport.gradingOverview.length > 0) && (
<div className="mt-6 p-4 bg-[--background-tertiary] rounded-lg">
<RadarPerformanceChart
title="Organizational Grading"
@@ -142,13 +142,13 @@ const CompanyWiki: React.FC = () => {
<Card>
<h4 className="text-lg font-semibold text-[--text-primary] mb-3">Strengths</h4>
<ul className="space-y-2">
{companyReport.organizationalStrengths.map((s: any, i) => <li key={i} className="text-[--text-secondary] text-sm"> {s.area}</li>)}
{(companyReport.organizationalStrengths || []).map((s: any, i) => <li key={i} className="text-[--text-secondary] text-sm"> {s.area || s}</li>)}
</ul>
</Card>
<Card>
<h4 className="text-lg font-semibold text-[--text-primary] mb-3">Risks</h4>
<ul className="space-y-2">
{companyReport.organizationalRisks.map((r, i) => <li key={i} className="text-[--text-secondary] text-sm"> {r}</li>)}
{(companyReport.organizationalRisks || []).map((r, i) => <li key={i} className="text-[--text-secondary] text-sm"> {r}</li>)}
</ul>
</Card>
<Card>
@@ -172,72 +172,152 @@ const CompanyWiki: React.FC = () => {
</div>
)}
{/* Company Profile - Onboarding Data */}
<Card className="mt-6">
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Company Profile</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Company Profile - Q&A Format from Onboarding */}
<div className="mt-6">
<h3 className="text-2xl font-semibold text-[--text-primary] mb-6">Company Profile</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{org?.mission && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Mission</h4>
<p className="text-[--text-secondary] text-sm">{org.mission}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What is your company's mission?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.mission}</p>
</div>
</div>
</Card>
)}
{org?.vision && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Vision</h4>
<p className="text-[--text-secondary] text-sm">{org.vision}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What is your company's vision?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.vision}</p>
</div>
</div>
</Card>
)}
{org?.evolution && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Company Evolution</h4>
<p className="text-[--text-secondary] text-sm">{org.evolution}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">How has your company evolved over time?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.evolution}</p>
</div>
</div>
</Card>
)}
{org?.advantages && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Competitive Advantages</h4>
<p className="text-[--text-secondary] text-sm">{org.advantages}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your competitive advantages?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.advantages}</p>
</div>
</div>
</Card>
)}
{org?.vulnerabilities && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Vulnerabilities</h4>
<p className="text-[--text-secondary] text-sm">{org.vulnerabilities}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your key vulnerabilities?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.vulnerabilities}</p>
</div>
</div>
</Card>
)}
{org?.shortTermGoals && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Short Term Goals</h4>
<p className="text-[--text-secondary] text-sm">{org.shortTermGoals}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your short-term goals?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.shortTermGoals}</p>
</div>
</div>
</Card>
)}
{org?.longTermGoals && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Long Term Goals</h4>
<p className="text-[--text-secondary] text-sm">{org.longTermGoals}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your long-term goals?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.longTermGoals}</p>
</div>
</div>
</Card>
)}
{org?.cultureDescription && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Culture</h4>
<p className="text-[--text-secondary] text-sm">{org.cultureDescription}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">How would you describe your company culture?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.cultureDescription}</p>
</div>
</div>
</Card>
)}
{org?.workEnvironment && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Work Environment</h4>
<p className="text-[--text-secondary] text-sm">{org.workEnvironment}</p>
</div>
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What is your work environment like?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.workEnvironment}</p>
</div>
</div>
</Card>
)}
{org?.additionalContext && (
<div className="md:col-span-2">
<h4 className="font-medium text-[--text-primary] mb-2">Additional Context</h4>
<p className="text-[--text-secondary] text-sm">{org.additionalContext}</p>
</div>
<Card className="p-4 lg:col-span-2">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">Any additional context about your company?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.additionalContext}</p>
</div>
</div>
</Card>
)}
</div>
</Card>
</div>
{org?.description && (
<Card className="mt-6">

View File

@@ -5,6 +5,7 @@ import { CompanyReport, Employee, Report } from '../types';
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
import ScoreBarList from '../components/charts/ScoreBarList';
import { SAMPLE_COMPANY_REPORT } from '../constants';
import ReportDetail from './ReportDetail';
interface EmployeeDataProps {
mode: 'submissions' | 'reports';
@@ -47,7 +48,7 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg">
<h3 className="text-sm font-medium text-[--text-secondary]">Departments</h3>
<p className="text-2xl `font-bold text-[--text-primary]">{report.overview.departmentBreakdown.length}</p>
<p className="text-2xl font-bold text-[--text-primary]">{report.overview.departmentBreakdown.length}</p>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg">
<h3 className="text-sm font-medium text-[--text-secondary]">Avg Performance</h3>
@@ -219,7 +220,8 @@ const EmployeeCard: React.FC<{
isOwner: boolean;
onGenerateReport?: (employee: Employee) => void;
isGeneratingReport?: boolean;
}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport }) => {
onViewReport?: (report: Report, employeeName: string) => void;
}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport, onViewReport }) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
@@ -243,13 +245,22 @@ const EmployeeCard: React.FC<{
</div>
<div className="flex space-x-2">
{report && (
<Button
size="sm"
variant="secondary"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? 'Hide' : 'View'} Report
</Button>
<>
<Button
size="sm"
variant="secondary"
onClick={() => onViewReport?.(report, employee.name)}
>
View Full Report
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? 'Hide' : 'View'} Summary
</Button>
</>
)}
{isOwner && mode === 'reports' && (
<Button
@@ -327,9 +338,11 @@ const EmployeeCard: React.FC<{
};
const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, saveReport, orgId } = useOrg();
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, saveReport, orgId } = useOrg();
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | Report; type: 'company' | 'employee'; employeeName?: string } | null>(null);
useEffect(() => {
// Load company report for owners
@@ -358,32 +371,19 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
try {
console.log('Generating report for employee:', employee.name, 'in org:', orgId);
// Call the API endpoint with orgId
const response = await fetch(`/api/employee-report`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
employeeId: employee.id,
orgId: orgId
}),
});
if (response.ok) {
const result = await response.json();
if (result.success && result.report) {
// Save the report using the context method
await saveReport(employee.id, result.report);
console.log('Report generated and saved successfully');
} else {
console.error('Report generation failed:', result.error || 'Unknown error');
}
// Use the OrgContext method instead of direct API call
const report = await generateEmployeeReport(employee);
if (report) {
console.log('Report generated and saved successfully for:', employee.name);
} else {
console.error('API call failed:', response.status, response.statusText);
console.error('Report generation failed for:', employee.name);
// Show user-friendly error
alert(`Failed to generate report for ${employee.name}. Please try again.`);
}
} catch (error) {
console.error('Error generating report:', error);
alert(`Error generating report for ${employee.name}: ${error.message}`);
} finally {
setGeneratingReports(prev => {
const newSet = new Set(prev);
@@ -391,7 +391,26 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
return newSet;
});
}
}; const currentUserIsOwner = isOwner(user?.uid || '');
};
const handleGenerateCompanyReport = async () => {
setGeneratingCompanyReport(true);
try {
console.log('Generating company report for org:', orgId);
const newReport = await generateCompanyReport();
console.log('Received new company report:', newReport);
setCompanyReport(newReport);
console.log('Company report generated and state updated successfully');
} catch (error) {
console.error('Error generating company report:', error);
alert(`Error generating company report: ${error.message}`);
} finally {
setGeneratingCompanyReport(false);
}
};
const currentUserIsOwner = isOwner(user?.uid || '');
// Filter employees based on user access
const visibleEmployees = currentUserIsOwner
@@ -412,8 +431,51 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
</div>
{/* Company Report - Only visible to owners in reports mode */}
{currentUserIsOwner && mode === 'reports' && companyReport && (
<CompanyReportCard report={companyReport} />
{currentUserIsOwner && mode === 'reports' && (
<div className="mb-6">
{companyReport ? (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-[--text-primary]">Company Report</h2>
<div className="flex space-x-2">
<Button
size="sm"
variant="secondary"
onClick={() => setSelectedReport({ report: companyReport, type: 'company' })}
>
View Full Report
</Button>
<Button
size="sm"
onClick={handleGenerateCompanyReport}
disabled={generatingCompanyReport}
>
{generatingCompanyReport ? 'Regenerating...' : 'Regenerate Report'}
</Button>
</div>
</div>
<CompanyReportCard report={companyReport} />
</div>
) : (
<Card>
<div className="text-center py-8">
<h3 className="text-lg font-semibold text-[--text-primary] mb-2">
Generate Company Report
</h3>
<p className="text-[--text-secondary] mb-4">
Create a comprehensive AI-powered report analyzing your organization's performance,
strengths, and recommendations based on employee data.
</p>
<Button
onClick={handleGenerateCompanyReport}
disabled={generatingCompanyReport}
>
{generatingCompanyReport ? 'Generating Report...' : 'Generate Company Report'}
</Button>
</div>
</Card>
)}
</div>
)}
{/* Employee Cards */}
@@ -442,10 +504,21 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
isOwner={currentUserIsOwner}
onGenerateReport={handleGenerateReport}
isGeneratingReport={generatingReports.has(employee.id)}
onViewReport={(report, employeeName) => setSelectedReport({ report, type: 'employee', employeeName })}
/>
))
)}
</div>
{/* Report Detail Modal */}
{selectedReport && (
<ReportDetail
report={selectedReport.report}
type={selectedReport.type}
employeeName={selectedReport.employeeName}
onClose={() => setSelectedReport(null)}
/>
)}
</div>
);
};

View File

@@ -15,17 +15,28 @@ const EmployeeQuestionnaire: React.FC = () => {
const location = useLocation();
const params = useParams();
const { user } = useAuth();
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
// Check if this is an invite-based flow (no auth/org needed)
const inviteCode = params.inviteCode;
const isInviteFlow = !!inviteCode;
// Only use org context for authenticated flows
let submitEmployeeAnswers, generateEmployeeReport, employees;
if (!isInviteFlow) {
const orgContext = useOrg();
({ submitEmployeeAnswers, generateEmployeeReport, employees } = orgContext);
} else {
// For invite flows, we don't need these functions from org context
submitEmployeeAnswers = null;
generateEmployeeReport = null;
employees = [];
}
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
// Check if this is an invite-based flow (no auth needed)
const inviteCode = params.inviteCode;
const isInviteFlow = !!inviteCode;
// Load invite details if this is an invite flow
useEffect(() => {
if (inviteCode) {
@@ -36,15 +47,24 @@ const EmployeeQuestionnaire: React.FC = () => {
const loadInviteDetails = async (code: string) => {
setIsLoadingInvite(true);
try {
const response = await fetch(`${API_URL}/api/invitations/${code}`);
// Use Cloud Function endpoint for invite status
const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
if (response.ok) {
const data = await response.json();
setInviteEmployee(data.employee);
setError('');
if (data.used) {
setError('This invitation has already been used');
} else if (data.employee) {
setInviteEmployee(data.employee);
setError('');
} else {
setError('Invalid invitation data');
}
} else {
setError('Invalid or expired invitation link');
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
setError(errorData.error || 'Invalid or expired invitation link');
}
} catch (err) {
console.error('Error loading invite details:', err);
setError('Failed to load invitation details');
} finally {
setIsLoadingInvite(false);
@@ -120,30 +140,39 @@ const EmployeeQuestionnaire: React.FC = () => {
}));
};
const submitViaInvite = async (employee: any, answers: EmployeeSubmissionAnswers, inviteCode: string) => {
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
try {
// First, consume the invite to mark it as used
const consumeResponse = await fetch(`${API_URL}/api/invitations/${inviteCode}/consume`, {
method: 'POST'
const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: inviteCode
})
});
if (!consumeResponse.ok) {
throw new Error('Failed to process invitation');
}
// Submit the questionnaire answers
const submitResponse = await fetch(`${API_URL}/api/employee-submissions`, {
// Get orgId from the consume response
const consumeData = await consumeResponse.json();
const orgId = consumeData.orgId;
// Submit the questionnaire answers using Cloud Function
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
employeeId: employee.id,
employee: employee,
answers: answers
inviteCode: inviteCode,
answers: answers,
orgId: orgId
})
});
if (!submitResponse.ok) {
throw new Error('Failed to submit questionnaire');
const errorData = await submitResponse.json();
throw new Error(errorData.error || 'Failed to submit questionnaire');
}
const result = await submitResponse.json();
@@ -223,7 +252,7 @@ const EmployeeQuestionnaire: React.FC = () => {
let result;
if (isInviteFlow) {
// Direct API submission for invite flow (no auth needed)
result = await submitViaInvite(currentEmployee, answers, inviteCode);
result = await submitViaInvite(answers, inviteCode);
} else {
// Use org context for authenticated flow
result = await submitEmployeeAnswers(currentEmployee.id, answers);
@@ -338,7 +367,7 @@ const EmployeeQuestionnaire: React.FC = () => {
<LinearProgress value={getProgressPercentage()} />
</div>
<form onSubmit={handleSubmit}>
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div className="space-y-6">
{visibleQuestions.map((question, index) => (
<Question
@@ -372,7 +401,7 @@ const EmployeeQuestionnaire: React.FC = () => {
disabled={isSubmitting || getProgressPercentage() < 70}
className="px-8 py-3"
>
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</div>

View File

@@ -45,7 +45,12 @@ const HelpAndSettings: React.FC = () => {
department: inviteForm.department.trim() || undefined
});
setInviteResult(`Invitation sent! Share this link: ${result.inviteLink}`);
setInviteResult(JSON.stringify({
success: true,
inviteLink: result.inviteLink,
emailLink: result.emailLink,
employeeName: result.employee.name
}));
setInviteForm({ name: '', email: '', role: '', department: '' });
} catch (error) {
console.error('Failed to send invitation:', error);
@@ -178,11 +183,61 @@ const HelpAndSettings: React.FC = () => {
</Button>
{inviteResult && (
<div className={`p-3 rounded-md text-sm ${inviteResult.includes('Failed')
? 'bg-red-50 text-red-800 border border-red-200'
: 'bg-green-50 text-green-800 border border-green-200'
}`}>
{inviteResult}
<div>
{inviteResult.includes('Failed') ? (
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
{inviteResult}
</div>
) : (
(() => {
try {
const result = JSON.parse(inviteResult);
return (
<div className="p-4 rounded-md bg-green-50 border border-green-200">
<h4 className="text-sm font-semibold text-green-800 mb-3">
Invitation sent to {result.employeeName}!
</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-green-700 mb-1">
Direct Link (share this with the employee):
</label>
<div className="flex gap-2">
<input
type="text"
value={result.inviteLink}
readOnly
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
/>
<Button
size="sm"
variant="secondary"
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
>
Copy
</Button>
</div>
</div>
<div>
<a
href={result.emailLink}
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
>
📧 Open Email Draft
</a>
</div>
</div>
</div>
);
} catch {
return (
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
{inviteResult}
</div>
);
}
})()
)}
</div>
)}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOrg } from '../contexts/OrgContext';
import { Card, Button } from '../components/UiKit';
@@ -48,6 +48,13 @@ interface OnboardingData {
const Onboarding: React.FC = () => {
const { org, upsertOrg, generateCompanyWiki } = useOrg();
const navigate = useNavigate();
useEffect(() => {
if (org?.onboardingCompleted) {
navigate('/reports', { replace: true });
}
}, [org, navigate]);
const [step, setStep] = useState(0);
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
const [formData, setFormData] = useState<OnboardingData>({
@@ -152,7 +159,7 @@ const Onboarding: React.FC = () => {
console.log('Org data saved successfully');
console.log('Generating company wiki...');
await generateCompanyWiki(newOrgData);
await generateCompanyWiki({ ...newOrgData, orgId: org!.orgId });
console.log('Company wiki generated successfully');
// Small delay to ensure states are updated, then redirect
@@ -674,7 +681,7 @@ const Onboarding: React.FC = () => {
{/* Progress indicator */}
<div className="mb-8">
<FigmaProgress
<FigmaProgress
currentStep={step + 1}
steps={steps.map((s, i) => ({ number: i + 1, title: s.title }))}
/>

View File

@@ -41,7 +41,7 @@ const OrgSelection: React.FC = () => {
const handleSelectOrg = (orgId: string) => {
selectOrganization(orgId);
// Check if the organization needs onboarding completion
const selectedOrg = organizations.find(org => org.orgId === orgId);
if (selectedOrg && !selectedOrg.onboardingCompleted && selectedOrg.role === 'owner') {

413
pages/ReportDetail.tsx Normal file
View File

@@ -0,0 +1,413 @@
import React from 'react';
import { Card, Button } from '../components/UiKit';
import { CompanyReport, Report } from '../types';
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
import ScoreBarList from '../components/charts/ScoreBarList';
interface ReportDetailProps {
report: CompanyReport | Report;
type: 'company' | 'employee';
employeeName?: string;
onClose: () => void;
}
const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName, onClose }) => {
if (type === 'company') {
const companyReport = report as CompanyReport;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-[--text-primary]">Company Report</h1>
<p className="text-[--text-secondary]">Last updated: {new Date(companyReport.createdAt).toLocaleDateString()}</p>
</div>
<div className="flex space-x-2">
<Button size="sm">Download as PDF</Button>
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Executive Summary */}
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Executive Summary</h2>
<p className="text-[--text-secondary] whitespace-pre-line leading-relaxed">
{companyReport.executiveSummary}
</p>
</Card>
{/* Overview Stats */}
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Company Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-blue-500 mb-1">{companyReport.overview.totalEmployees}</div>
<div className="text-sm text-[--text-secondary]">Total Employees</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-green-500 mb-1">{companyReport.overview.departmentBreakdown?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Departments</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-purple-500 mb-1">{companyReport.overview.averagePerformanceScore || 'N/A'}</div>
<div className="text-sm text-[--text-secondary]">Avg Performance</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-orange-500 mb-1">{companyReport.overview.riskLevel || 'Low'}</div>
<div className="text-sm text-[--text-secondary]">Risk Level</div>
</div>
</div>
</Card>
{/* Key Personnel Changes */}
{companyReport.keyPersonnelChanges && companyReport.keyPersonnelChanges.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
Key Personnel Changes
</h2>
<div className="space-y-3">
{companyReport.keyPersonnelChanges.map((change, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold text-[--text-primary]">{change.employeeName}</h3>
<p className="text-sm text-[--text-secondary]">{change.role} {change.department}</p>
</div>
<span className={`px-3 py-1 text-xs rounded-full font-medium ${
change.changeType === 'departure' ? 'bg-red-100 text-red-800' :
change.changeType === 'promotion' ? 'bg-green-100 text-green-800' :
'bg-blue-100 text-blue-800'
}`}>
{change.changeType}
</span>
</div>
<p className="text-[--text-secondary] text-sm">{change.impact}</p>
</div>
))}
</div>
</Card>
)}
{/* Immediate Hiring Needs */}
{companyReport.immediateHiringNeeds && companyReport.immediateHiringNeeds.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-red-500 rounded-full mr-3"></span>
Immediate Hiring Needs
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{companyReport.immediateHiringNeeds.map((need, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-[--text-primary]">{need.role}</h3>
<span className={`px-2 py-1 text-xs rounded-full font-medium ${
need.urgency === 'high' ? 'bg-red-100 text-red-800' :
need.urgency === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{need.urgency}
</span>
</div>
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
<p className="text-sm text-[--text-secondary]">{need.reason}</p>
</div>
))}
</div>
</Card>
)}
{/* Forward Operating Plan */}
{companyReport.forwardOperatingPlan && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
Forward Operating Plan
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-semibold text-[--text-primary] mb-3">Next Quarter Goals</h3>
<ul className="space-y-2">
{companyReport.forwardOperatingPlan.nextQuarterGoals?.map((goal, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
{goal}
</li>
))}
</ul>
</div>
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-semibold text-[--text-primary] mb-3">Key Initiatives</h3>
<ul className="space-y-2">
{companyReport.forwardOperatingPlan.keyInitiatives?.map((initiative, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
{initiative}
</li>
))}
</ul>
</div>
</div>
</Card>
)}
{/* Organizational Strengths */}
{companyReport.organizationalStrengths && companyReport.organizationalStrengths.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
Organizational Strengths
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{companyReport.organizationalStrengths.map((strength, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex items-start space-x-3">
<span className="text-3xl">{strength.icon || '💪'}</span>
<div>
<h3 className="font-semibold text-[--text-primary] mb-1">{strength.area || strength}</h3>
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{/* Grading Overview */}
{companyReport.gradingOverview && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-indigo-500 rounded-full mr-3"></span>
Grading Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{Object.entries(companyReport.gradingOverview).map(([category, score], idx) => (
<div key={idx} className="text-center p-4 bg-[--background-tertiary] rounded-lg">
<div className="text-3xl font-bold text-[--text-primary] mb-2">{score}/5</div>
<div className="text-sm text-[--text-secondary] capitalize">
{category.replace(/([A-Z])/g, ' $1').trim()}
</div>
</div>
))}
</div>
</Card>
)}
{/* Organizational Impact Summary */}
{companyReport.organizationalImpactSummary && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
Organizational Impact Summary
</h2>
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<p className="text-[--text-secondary] leading-relaxed">
{companyReport.organizationalImpactSummary}
</p>
</div>
</Card>
)}
</div>
</div>
</div>
);
} else {
const employeeReport = report as Report;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-[--text-primary]">{employeeName}'s Performance Report</h1>
<p className="text-[--text-secondary]">{employeeReport.employee?.role} • {employeeReport.employee?.department}</p>
</div>
<div className="flex space-x-2">
<Button size="sm">Download as PDF</Button>
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Self-Reported Role & Output */}
{employeeReport.roleAndOutput && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Self-Reported Role & Output</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium text-[--text-primary] mb-2">Responsibilities</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">{employeeReport.roleAndOutput.responsibilities}</p>
</div>
<div className="space-y-3">
<div>
<h3 className="font-medium text-[--text-primary] mb-1">Clarity on Role</h3>
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.clarityOnRole}</p>
</div>
<div>
<h3 className="font-medium text-[--text-primary] mb-1">Self-Rated Output</h3>
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.selfRatedOutput}</p>
</div>
</div>
</div>
</Card>
)}
{/* Performance Charts */}
{employeeReport.grading?.[0]?.scores && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Performance Analysis</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-[--background-tertiary] rounded-lg p-6">
<RadarPerformanceChart
title="Performance Profile"
data={employeeReport.grading[0].scores.map(s => ({
label: s.subject,
value: (s.value / s.fullMark) * 100
}))}
/>
</div>
<div className="bg-[--background-tertiary] rounded-lg p-6">
<ScoreBarList
title="Score Breakdown"
items={employeeReport.grading[0].scores.map(s => ({
label: s.subject,
value: s.value,
max: s.fullMark
}))}
/>
</div>
</div>
</Card>
)}
{/* Behavioral & Psychological Insights */}
{employeeReport.insights && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Behavioral & Psychological Insights</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium text-[--text-primary] mb-3">Personality Traits</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed mb-4">
{employeeReport.insights.personalityTraits || 'No personality traits data available.'}
</p>
<h3 className="font-medium text-[--text-primary] mb-3">Self-awareness</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">
{employeeReport.insights.selfAwareness || 'No self-awareness data available.'}
</p>
</div>
<div>
<h3 className="font-medium text-[--text-primary] mb-3">Psychological Indicators</h3>
<ul className="space-y-2 mb-4">
{employeeReport.insights.psychologicalIndicators?.map((indicator, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
{indicator}
</li>
)) || <li className="text-sm text-[--text-secondary]">No psychological indicators available.</li>}
</ul>
<h3 className="font-medium text-[--text-primary] mb-3">Growth Desire</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">
{employeeReport.insights.growthDesire || 'No growth desire data available.'}
</p>
</div>
</div>
</Card>
)}
{/* Strengths & Weaknesses */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
Strengths
</h2>
<div className="space-y-2">
{employeeReport.insights?.strengths?.map((strength, idx) => (
<div key={idx} className="flex items-center space-x-2">
<span className="text-green-500">✓</span>
<span className="text-sm text-[--text-secondary]">{strength}</span>
</div>
))}
</div>
</Card>
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
Development Areas
</h2>
<div className="space-y-2">
{employeeReport.insights?.weaknesses?.map((weakness, idx) => (
<div key={idx} className="flex items-center space-x-2">
<span className="text-orange-500">!</span>
<span className="text-sm text-[--text-secondary]">{weakness}</span>
</div>
))}
</div>
</Card>
</div>
{/* Opportunities */}
{employeeReport.opportunities && employeeReport.opportunities.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
Opportunities
</h2>
<div className="space-y-4">
{employeeReport.opportunities.map((opp, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-medium text-[--text-primary] mb-2">{opp.roleAdjustment || 'Opportunity'}</h3>
<p className="text-sm text-[--text-secondary]">{opp.accountabilitySupport || opp.description}</p>
</div>
))}
</div>
</Card>
)}
{/* Risks */}
{employeeReport.risks && employeeReport.risks.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-3"></span>
Risks
</h2>
<div className="space-y-2">
{employeeReport.risks.map((risk, idx) => (
<div key={idx} className="flex items-start space-x-2 p-3 bg-yellow-50 rounded-lg">
<span className="text-yellow-500 mt-0.5"></span>
<span className="text-sm text-gray-700">{risk}</span>
</div>
))}
</div>
</Card>
)}
{/* Recommendations */}
{employeeReport.recommendations && employeeReport.recommendations.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
Recommendations
</h2>
<div className="space-y-3">
{employeeReport.recommendations.map((rec, idx) => (
<div key={idx} className="flex items-start space-x-3 p-3 bg-[--background-tertiary] rounded-lg">
<span className="w-2 h-2 bg-purple-500 rounded-full mt-2 flex-shrink-0"></span>
<span className="text-sm text-[--text-secondary]">{rec}</span>
</div>
))}
</div>
</Card>
)}
</div>
</div>
</div>
);
}
};
export default ReportDetail;