- 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>
391 lines
20 KiB
TypeScript
391 lines
20 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useTheme } from '../contexts/ThemeContext';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { useOrg } from '../contexts/OrgContext';
|
|
import { Card, Button } from '../components/UiKit';
|
|
import { Theme } from '../types';
|
|
|
|
const HelpAndSettings: React.FC = () => {
|
|
const { theme, setTheme } = useTheme();
|
|
const { user, signOutUser } = useAuth();
|
|
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
|
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
|
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
|
const [isInviting, setIsInviting] = useState(false);
|
|
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await signOutUser();
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
}
|
|
};
|
|
|
|
const handleRestartOnboarding = async () => {
|
|
try {
|
|
await upsertOrg({ onboardingCompleted: false });
|
|
// The RequireOnboarding component will redirect automatically
|
|
} catch (error) {
|
|
console.error('Failed to restart onboarding:', error);
|
|
}
|
|
};
|
|
|
|
const handleInviteEmployee = async () => {
|
|
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
|
|
|
setIsInviting(true);
|
|
setInviteResult(null);
|
|
|
|
try {
|
|
const result = await issueInviteViaApi({
|
|
name: inviteForm.name.trim(),
|
|
email: inviteForm.email.trim(),
|
|
role: inviteForm.role.trim() || undefined,
|
|
department: inviteForm.department.trim() || undefined
|
|
});
|
|
|
|
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);
|
|
setInviteResult('Failed to send invitation. Please try again.');
|
|
} finally {
|
|
setIsInviting(false);
|
|
}
|
|
};
|
|
|
|
const renderSettings = () => (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
|
Theme
|
|
</label>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => setTheme(Theme.Light)}
|
|
>
|
|
Light
|
|
</Button>
|
|
<Button
|
|
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => setTheme(Theme.Dark)}
|
|
>
|
|
Dark
|
|
</Button>
|
|
<Button
|
|
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => setTheme(Theme.System)}
|
|
>
|
|
System
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<span className="text-sm text-[--text-secondary]">Company:</span>
|
|
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
|
<div className="font-medium text-[--text-primary]">
|
|
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
|
</div>
|
|
</div>
|
|
<div className="pt-4">
|
|
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
|
Restart Onboarding
|
|
</Button>
|
|
<p className="text-xs text-[--text-secondary] mt-2">
|
|
This will reset your company profile and require you to complete the setup process again.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={inviteForm.name}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="John Doe"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
Email *
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={inviteForm.email}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="john.doe@company.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
Role
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={inviteForm.role}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="Senior Developer"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
Department
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={inviteForm.department}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="Engineering"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleInviteEmployee}
|
|
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
|
className="w-full"
|
|
>
|
|
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
|
</Button>
|
|
|
|
{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>
|
|
)}
|
|
|
|
<p className="text-xs text-[--text-secondary]">
|
|
The invited employee will receive an email with instructions to join your organization.
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<span className="text-sm text-[--text-secondary]">Email:</span>
|
|
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
|
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
|
</div>
|
|
<div className="pt-4">
|
|
<Button variant="secondary" onClick={handleLogout}>
|
|
Sign Out
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
|
<div className="space-y-3">
|
|
<Button variant="secondary" className="w-full justify-start">
|
|
Export My Data
|
|
</Button>
|
|
<Button variant="secondary" className="w-full justify-start">
|
|
Privacy Settings
|
|
</Button>
|
|
<Button variant="secondary" className="w-full justify-start text-red-600">
|
|
Delete Account
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
const renderHelp = () => (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Complete the onboarding process to configure your company information and preferences.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Invite team members and add their basic information to start generating reports.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Use AI-powered reports to gain insights into employee performance and organizational health.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
|
<p className="text-[--text-secondary] text-sm">
|
|
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
|
<div className="space-y-3">
|
|
<Button variant="secondary" className="w-full justify-start">
|
|
📧 Email Support
|
|
</Button>
|
|
<Button variant="secondary" className="w-full justify-start">
|
|
💬 Live Chat
|
|
</Button>
|
|
<Button variant="secondary" className="w-full justify-start">
|
|
📚 Documentation
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="p-6 max-w-4xl mx-auto">
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
|
<p className="text-[--text-secondary] mt-1">
|
|
Manage your account and get help
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<div className="flex space-x-4 border-b border-[--border-color]">
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
|
? 'border-blue-500 text-blue-500'
|
|
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
|
}`}
|
|
>
|
|
Settings
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('help')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
|
? 'border-blue-500 text-blue-500'
|
|
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
|
}`}
|
|
>
|
|
Help
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HelpAndSettings;
|