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:
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 }))}
|
||||
/>
|
||||
|
||||
@@ -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
413
pages/ReportDetail.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user