Fix organization setup flow: redirect to onboarding for incomplete setup
This commit is contained in:
137
pages/Chat.tsx
Normal file
137
pages/Chat.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { CHAT_STARTERS } from '../constants';
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const { employees, reports, generateEmployeeReport } = useOrg();
|
||||
const [messages, setMessages] = useState<Array<{ id: string, role: 'user' | 'assistant', text: string }>>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>('');
|
||||
const selectedReport = selectedEmployeeId ? reports[selectedEmployeeId] : undefined;
|
||||
|
||||
const dynamicStarters = useMemo(() => {
|
||||
if (!selectedReport) return CHAT_STARTERS.slice(0, 4);
|
||||
const strengths = selectedReport.insights.strengths?.slice(0, 2) || [];
|
||||
const weaknesses = selectedReport.insights.weaknesses?.slice(0, 1) || [];
|
||||
const risk = selectedReport.retentionRisk;
|
||||
const starters: string[] = [];
|
||||
if (strengths[0]) starters.push(`How can we further leverage ${strengths[0]} for cross-team impact?`);
|
||||
if (weaknesses[0]) starters.push(`What is an actionable plan to address ${weaknesses[0]} this quarter?`);
|
||||
if (risk) starters.push(`What factors contribute to ${selectedReport.employeeId}'s ${risk} retention risk?`);
|
||||
starters.push(`Is ${selectedReport.employeeId} a candidate for expanded scope or leadership?`);
|
||||
while (starters.length < 4) starters.push(CHAT_STARTERS[starters.length] || 'Provide an organizational insight.');
|
||||
return starters.slice(0, 4);
|
||||
}, [selectedReport]);
|
||||
|
||||
const handleSend = async (message?: string) => {
|
||||
const textToSend = message || input;
|
||||
if (!textToSend.trim()) return;
|
||||
|
||||
const userMessage = { id: Date.now().toString(), role: 'user' as const, text: textToSend };
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate AI response (placeholder for server /api/chat usage)
|
||||
setTimeout(() => {
|
||||
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}".`
|
||||
};
|
||||
setMessages(prev => [...prev, aiResponse]);
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto h-full flex flex-col">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">Chat with AI</h1>
|
||||
<p className="text-[--text-secondary] mt-1">Ask questions about your employees and organization</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-[--text-secondary]">Focus Employee:</label>
|
||||
<select
|
||||
className="px-2 py-1 text-sm bg-[--background-secondary] border border-[--border-color] rounded"
|
||||
value={selectedEmployeeId}
|
||||
onChange={e => setSelectedEmployeeId(e.target.value)}
|
||||
>
|
||||
<option value="">(Organization)</option>
|
||||
{employees.map(emp => <option key={emp.id} value={emp.id}>{emp.name}</option>)}
|
||||
</select>
|
||||
{selectedEmployeeId && !selectedReport && (
|
||||
<Button size="sm" variant="secondary" onClick={() => generateEmployeeReport(employees.find(e => e.id === selectedEmployeeId)!)}>
|
||||
Generate Report
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.length === 0 && (
|
||||
<Card className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Get started with these questions:</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{dynamicStarters.map((starter, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="text-left justify-start"
|
||||
onClick={() => handleSend(starter)}
|
||||
>
|
||||
{starter}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto mb-4 space-y-4">
|
||||
{messages.map(message => (
|
||||
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[70%] p-4 rounded-lg ${message.role === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-[--background-secondary] text-[--text-primary]'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-[--background-secondary] text-[--text-primary] p-4 rounded-lg">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card padding="sm">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="Ask about employees, reports, or company insights..."
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<Button onClick={() => handleSend()} disabled={isLoading || !input.trim()}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
252
pages/CompanyWiki.tsx
Normal file
252
pages/CompanyWiki.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { FigmaAlert } from '../components/figma/FigmaAlert';
|
||||
import { CompanyReport } from '../types';
|
||||
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
||||
|
||||
const CompanyWiki: React.FC = () => {
|
||||
const { org, employees, getFullCompanyReportHistory, generateCompanyWiki } = useOrg();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const history = await getFullCompanyReportHistory();
|
||||
if (history.length) setCompanyReport(history[0]);
|
||||
} catch (e) {
|
||||
console.error('Failed loading company report history', e);
|
||||
}
|
||||
})();
|
||||
}, [getFullCompanyReportHistory]);
|
||||
|
||||
const generateReport = async () => {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const report = await generateCompanyWiki();
|
||||
setCompanyReport(report);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError('Failed to generate company wiki');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">Company Wiki</h1>
|
||||
<p className="text-[--text-secondary] mt-1">
|
||||
Organization overview and insights
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">Company Info</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Name:</span>
|
||||
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Industry:</span>
|
||||
<div className="font-medium text-[--text-primary]">{org?.industry}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Size:</span>
|
||||
<div className="font-medium text-[--text-primary]">{org?.size}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">Team Stats</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Total Employees:</span>
|
||||
<div className="font-medium text-[--text-primary]">{employees.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Departments:</span>
|
||||
<div className="font-medium text-[--text-primary]">
|
||||
{[...new Set(employees.map(e => e.department))].length}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Roles:</span>
|
||||
<div className="font-medium text-[--text-primary]">
|
||||
{[...new Set(employees.map(e => e.role))].length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">Quick Actions</h3>
|
||||
<div className="space-y-3">
|
||||
<Button onClick={generateReport} disabled={isGenerating} className="w-full">
|
||||
{isGenerating ? 'Generating...' : companyReport ? 'Regenerate Company Wiki' : 'Generate Company Wiki'}
|
||||
</Button>
|
||||
{error && <FigmaAlert type="error" message={error} />}
|
||||
{!companyReport && !isGenerating && (
|
||||
<FigmaAlert type="info" message="No company wiki generated yet. Use the button above to create one." />
|
||||
)}
|
||||
<Button variant="secondary" className="w-full" disabled={!companyReport}>
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{companyReport && (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h3 className="text-xl font-semibold text-[--text-primary] mb-4">Executive Summary</h3>
|
||||
<p className="text-[--text-secondary] whitespace-pre-line mb-4">{companyReport.executiveSummary}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-[--background-secondary] rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-500">{companyReport.overview.totalEmployees}</div>
|
||||
<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-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-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-sm text-[--text-secondary]">Risks</div>
|
||||
</div>
|
||||
</div>
|
||||
{companyReport.gradingOverview && (
|
||||
<div className="mt-6 p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<RadarPerformanceChart
|
||||
title="Organizational Grading"
|
||||
data={companyReport.gradingOverview.map((g: any) => ({
|
||||
label: g.category || g.department || g.subject || 'Metric',
|
||||
value: g.value ?? g.averageScore ?? 0
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<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>)}
|
||||
</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>)}
|
||||
</ul>
|
||||
</Card>
|
||||
<Card>
|
||||
<h4 className="text-lg font-semibold text-[--text-primary] mb-3">Forward Plan</h4>
|
||||
<div>
|
||||
<h5 className="font-medium text-[--text-primary] text-sm mb-1">Goals</h5>
|
||||
<ul className="mb-2 list-disc list-inside text-[--text-secondary] text-sm space-y-1">
|
||||
{(companyReport.operatingPlan?.nextQuarterGoals || companyReport.forwardOperatingPlan?.quarterlyGoals || []).map((g: string, i: number) => <li key={i}>{g}</li>)}
|
||||
</ul>
|
||||
<h5 className="font-medium text-[--text-primary] text-sm mb-1">Resource Needs</h5>
|
||||
<ul className="mb-2 list-disc list-inside text-[--text-secondary] text-sm space-y-1">
|
||||
{(companyReport.operatingPlan?.resourceNeeds || companyReport.forwardOperatingPlan?.resourceNeeds || []).map((g: string, i: number) => <li key={i}>{g}</li>)}
|
||||
</ul>
|
||||
<h5 className="font-medium text-[--text-primary] text-sm mb-1">Risk Mitigation</h5>
|
||||
<ul className="list-disc list-inside text-[--text-secondary] text-sm space-y-1">
|
||||
{(companyReport.operatingPlan?.riskMitigation || companyReport.forwardOperatingPlan?.riskMitigation || []).map((g: string, i: number) => <li key={i}>{g}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</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">
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{org?.description && (
|
||||
<Card className="mt-6">
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">About</h3>
|
||||
<p className="text-[--text-secondary]">{org.description}</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyWiki;
|
||||
215
pages/DebugEmployee.tsx
Normal file
215
pages/DebugEmployee.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card } from '../components/UiKit';
|
||||
|
||||
const DebugEmployee: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { employees, org } = useOrg();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-8 text-center">
|
||||
Employee Debug Information
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Current User Info */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Current User
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="font-medium">Email:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">{user?.email || 'Not logged in'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Display Name:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">{user?.displayName || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">UID:</span>
|
||||
<span className="ml-2 text-[--text-secondary] text-xs break-all">{user?.uid || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Organization Info */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Organization
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="font-medium">Name:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">{org?.name || 'Not set'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Org ID:</span>
|
||||
<span className="ml-2 text-[--text-secondary] text-xs break-all">{org?.orgId || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Onboarding Complete:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">{org?.onboardingCompleted ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Employee Matching Analysis */}
|
||||
<Card className="p-6 lg:col-span-2">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Employee Matching Analysis
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="font-medium">Total Employees:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">{employees.length}</span>
|
||||
</div>
|
||||
|
||||
{user?.email && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-[--text-primary]">Matching Results:</h3>
|
||||
|
||||
{/* Exact match */}
|
||||
<div className="pl-4">
|
||||
<span className="text-sm font-medium">Exact Email Match:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">
|
||||
{employees.find(emp => emp.email === user.email) ? '✅ Found' : '❌ Not found'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Case insensitive match */}
|
||||
<div className="pl-4">
|
||||
<span className="text-sm font-medium">Case-Insensitive Match:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">
|
||||
{employees.find(emp => emp.email?.toLowerCase() === user.email?.toLowerCase()) ? '✅ Found' : '❌ Not found'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Domain match */}
|
||||
<div className="pl-4">
|
||||
<span className="text-sm font-medium">Same Domain Match:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">
|
||||
{(() => {
|
||||
const userDomain = user.email?.split('@')[1];
|
||||
const domainMatch = employees.find(emp => emp.email?.split('@')[1] === userDomain);
|
||||
return domainMatch ? `✅ Found: ${domainMatch.name} (${domainMatch.email})` : '❌ Not found';
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Username partial match */}
|
||||
<div className="pl-4">
|
||||
<span className="text-sm font-medium">Username Partial Match:</span>
|
||||
<span className="ml-2 text-[--text-secondary]">
|
||||
{(() => {
|
||||
const username = user.email?.split('@')[0];
|
||||
const partialMatch = employees.find(emp =>
|
||||
emp.email?.toLowerCase().includes(username?.toLowerCase() || '')
|
||||
);
|
||||
return partialMatch ? `✅ Found: ${partialMatch.name} (${partialMatch.email})` : '❌ Not found';
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* All Employees List */}
|
||||
<Card className="p-6 lg:col-span-2">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
All Employees ({employees.length})
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{employees.length === 0 ? (
|
||||
<p className="text-[--text-secondary] italic">No employees found</p>
|
||||
) : (
|
||||
employees.map((employee, index) => (
|
||||
<div key={employee.id} className="border border-[--border-color] rounded-lg p-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<div>
|
||||
<span className="font-medium">Name:</span>
|
||||
<span className="ml-2">{employee.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Email:</span>
|
||||
<span className="ml-2 text-sm">{employee.email || 'Not set'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Role:</span>
|
||||
<span className="ml-2 text-sm">{employee.role || 'Not set'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-medium">ID:</span>
|
||||
<span className="ml-2 text-xs text-[--text-secondary] break-all">{employee.id}</span>
|
||||
</div>
|
||||
{user?.email && employee.email && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="font-medium">Match Analysis:</span>
|
||||
<span className="ml-2">
|
||||
{employee.email === user.email && (
|
||||
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs mr-1">Exact</span>
|
||||
)}
|
||||
{employee.email?.toLowerCase() === user.email?.toLowerCase() && employee.email !== user.email && (
|
||||
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs mr-1">Case Diff</span>
|
||||
)}
|
||||
{employee.email?.split('@')[1] === user.email?.split('@')[1] && (
|
||||
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs mr-1">Same Domain</span>
|
||||
)}
|
||||
{employee.email?.toLowerCase().includes(user.email?.split('@')[0]?.toLowerCase() || '') && (
|
||||
<span className="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs mr-1">Username Match</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-6 lg:col-span-2">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Quick Actions
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href="#/employee-questionnaire"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm"
|
||||
>
|
||||
Try Traditional Questionnaire
|
||||
</a>
|
||||
<a
|
||||
href="#/employee-questionnaire-steps"
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors text-sm"
|
||||
>
|
||||
Try Stepped Questionnaire
|
||||
</a>
|
||||
<a
|
||||
href="#/reports"
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors text-sm"
|
||||
>
|
||||
Go to Reports
|
||||
</a>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors text-sm"
|
||||
>
|
||||
Refresh Page
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebugEmployee;
|
||||
453
pages/EmployeeData.tsx
Normal file
453
pages/EmployeeData.tsx
Normal file
@@ -0,0 +1,453 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { CompanyReport, Employee, Report } from '../types';
|
||||
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
||||
import ScoreBarList from '../components/charts/ScoreBarList';
|
||||
import { SAMPLE_COMPANY_REPORT } from '../constants';
|
||||
|
||||
interface EmployeeDataProps {
|
||||
mode: 'submissions' | 'reports';
|
||||
}
|
||||
|
||||
const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="mb-6 border-l-4 border-blue-500">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||||
ZM
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[--text-primary]">Company Report</h2>
|
||||
<p className="text-sm text-[--text-secondary]">
|
||||
Last updated: {new Date(report.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? 'Collapse' : 'View Details'}
|
||||
</Button>
|
||||
<Button size="sm">Download as PDF</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Section - Always Visible */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="bg-[--background-tertiary] p-4 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-[--text-secondary]">Total Employees</h3>
|
||||
<p className="text-2xl font-bold text-[--text-primary]">{report.overview.totalEmployees}</p>
|
||||
</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>
|
||||
</div>
|
||||
<div className="bg-[--background-tertiary] p-4 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-[--text-secondary]">Avg Performance</h3>
|
||||
<p className="text-2xl font-bold text-[--text-primary]">{report.overview.averagePerformanceScore}/5</p>
|
||||
</div>
|
||||
<div className="bg-[--background-tertiary] p-4 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-[--text-secondary]">Risk Level</h3>
|
||||
<p className="text-2xl font-bold text-[--text-primary]">{report.overview.riskLevel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-6 space-y-6">
|
||||
{/* Key Personnel Changes */}
|
||||
{report.keyPersonnelChanges && report.keyPersonnelChanges.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-orange-500 rounded-full mr-2"></span>
|
||||
Key Personnel Changes
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{report.keyPersonnelChanges.map((change, idx) => (
|
||||
<div key={idx} className="p-3 bg-[--background-tertiary] rounded-lg">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="font-medium text-[--text-primary]">{change.employeeName}</p>
|
||||
<p className="text-sm text-[--text-secondary]">{change.role} - {change.department}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${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-sm text-[--text-secondary] mt-2">{change.impact}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Immediate Hiring Needs */}
|
||||
{report.immediateHiringNeeds && report.immediateHiringNeeds.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
|
||||
Immediate Hiring Needs
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{report.immediateHiringNeeds.map((need, idx) => (
|
||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-medium text-[--text-primary]">{need.role}</h4>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${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} priority
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Forward Operating Plan */}
|
||||
{report.forwardOperatingPlan && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>
|
||||
Forward Operating Plan
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Next Quarter Goals</h4>
|
||||
<ul className="space-y-1">
|
||||
{report.forwardOperatingPlan.nextQuarterGoals.map((goal, 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>
|
||||
{goal}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Key Initiatives</h4>
|
||||
<ul className="space-y-1">
|
||||
{report.forwardOperatingPlan.keyInitiatives.map((initiative, idx) => (
|
||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
||||
<span className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
||||
{initiative}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Organizational Strengths */}
|
||||
{report.organizationalStrengths && report.organizationalStrengths.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
Organizational Strengths
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{report.organizationalStrengths.map((strength, idx) => (
|
||||
<div key={idx} className="p-3 bg-[--background-tertiary] rounded-lg">
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-2xl">{strength.icon}</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary]">{strength.area}</h4>
|
||||
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Organizational Impact Summary */}
|
||||
{report.organizationalImpactSummary && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-purple-500 rounded-full mr-2"></span>
|
||||
Organizational Impact Summary
|
||||
</h3>
|
||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<p className="text-[--text-secondary] text-sm leading-relaxed">
|
||||
{report.organizationalImpactSummary}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grading Overview */}
|
||||
{report.gradingOverview && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3 flex items-center">
|
||||
<span className="w-2 h-2 bg-indigo-500 rounded-full mr-2"></span>
|
||||
Grading Overview
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Object.entries(report.gradingOverview).map(([category, score], idx) => (
|
||||
<div key={idx} className="text-center p-4 bg-[--background-tertiary] rounded-lg">
|
||||
<div className="text-2xl font-bold text-[--text-primary] mb-1">{score}/5</div>
|
||||
<div className="text-sm text-[--text-secondary] capitalize">{category.replace(/([A-Z])/g, ' $1').trim()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const EmployeeCard: React.FC<{
|
||||
employee: Employee;
|
||||
report?: Report;
|
||||
mode: 'submissions' | 'reports';
|
||||
isOwner: boolean;
|
||||
onGenerateReport?: (employee: Employee) => void;
|
||||
isGeneratingReport?: boolean;
|
||||
}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||||
{employee.initials}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[--text-primary]">{employee.name}</h3>
|
||||
<p className="text-sm text-[--text-secondary]">
|
||||
{employee.role} {employee.department && `• ${employee.department}`}
|
||||
</p>
|
||||
</div>
|
||||
{employee.isOwner && (
|
||||
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">
|
||||
Owner
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{report && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? 'Hide' : 'View'} Report
|
||||
</Button>
|
||||
)}
|
||||
{isOwner && mode === 'reports' && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onGenerateReport?.(employee)}
|
||||
disabled={isGeneratingReport}
|
||||
>
|
||||
{isGeneratingReport ? 'Generating...' : report ? 'Regenerate Report' : 'Generate Report'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && report && (
|
||||
<div className="mt-4 pt-4 border-t border-[--border-color] space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Role & Output</h4>
|
||||
<p className="text-sm text-[--text-secondary]">{report.roleAndOutput.responsibilities}</p>
|
||||
</div>
|
||||
|
||||
{report.grading?.[0]?.scores && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-[--background-tertiary] rounded-lg p-4">
|
||||
<RadarPerformanceChart
|
||||
title="Performance Profile"
|
||||
data={report.grading[0].scores.map(s => ({ label: s.subject, value: (s.value / s.fullMark) * 100 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-[--background-tertiary] rounded-lg p-4">
|
||||
<ScoreBarList
|
||||
title="Score Breakdown"
|
||||
items={report.grading[0].scores.map(s => ({ label: s.subject, value: s.value, max: s.fullMark }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Key Strengths</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{report.insights.strengths.map((strength, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
||||
{strength}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Development Areas</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{report.insights.weaknesses.map((weakness, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-orange-100 text-orange-800 text-xs rounded-full">
|
||||
{weakness}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Recommendations</h4>
|
||||
<ul className="space-y-1">
|
||||
{report.recommendations.map((rec, 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>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
|
||||
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, saveReport, orgId } = useOrg();
|
||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
// Load company report for owners
|
||||
const loadCompanyReport = async () => {
|
||||
if (isOwner(user?.uid || '') && mode === 'reports') {
|
||||
try {
|
||||
const history = await getFullCompanyReportHistory();
|
||||
if (history.length > 0) {
|
||||
setCompanyReport(history[0]);
|
||||
} else {
|
||||
// Fallback to sample report if no real report exists
|
||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load company report:', error);
|
||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
||||
}
|
||||
}
|
||||
};
|
||||
loadCompanyReport();
|
||||
}, [isOwner, user?.uid, mode, getFullCompanyReportHistory]);
|
||||
|
||||
const handleGenerateReport = async (employee: Employee) => {
|
||||
setGeneratingReports(prev => new Set(prev).add(employee.id));
|
||||
|
||||
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');
|
||||
}
|
||||
} else {
|
||||
console.error('API call failed:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating report:', error);
|
||||
} finally {
|
||||
setGeneratingReports(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(employee.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
}; const currentUserIsOwner = isOwner(user?.uid || '');
|
||||
|
||||
// Filter employees based on user access
|
||||
const visibleEmployees = currentUserIsOwner
|
||||
? employees
|
||||
: employees.filter(emp => emp.id === user?.uid);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">
|
||||
{mode === 'submissions' ? 'Employee Submissions' : 'Employee Reports'}
|
||||
</h1>
|
||||
<p className="text-[--text-secondary] mt-1">
|
||||
{mode === 'submissions'
|
||||
? 'Manage employee data and submissions'
|
||||
: 'View AI-generated insights and reports'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Company Report - Only visible to owners in reports mode */}
|
||||
{currentUserIsOwner && mode === 'reports' && companyReport && (
|
||||
<CompanyReportCard report={companyReport} />
|
||||
)}
|
||||
|
||||
{/* Employee Cards */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary]">
|
||||
{currentUserIsOwner ? 'All Employees' : 'Your Information'}
|
||||
</h2>
|
||||
{visibleEmployees.length === 0 ? (
|
||||
<Card>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-[--text-secondary]">No employees found.</p>
|
||||
{currentUserIsOwner && (
|
||||
<Button className="mt-4" size="sm">
|
||||
Invite First Employee
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
visibleEmployees.map(employee => (
|
||||
<EmployeeCard
|
||||
key={employee.id}
|
||||
employee={employee}
|
||||
report={reports[employee.id]}
|
||||
mode={mode}
|
||||
isOwner={currentUserIsOwner}
|
||||
onGenerateReport={handleGenerateReport}
|
||||
isGeneratingReport={generatingReports.has(employee.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeData;
|
||||
390
pages/EmployeeQuestionnaire.tsx
Normal file
390
pages/EmployeeQuestionnaire.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
|
||||
import { Question } from '../components/ui/Question';
|
||||
import { QuestionInput } from '../components/ui/QuestionInput';
|
||||
import { LinearProgress } from '../components/ui/Progress';
|
||||
import { Alert } from '../components/ui/Alert';
|
||||
import { API_URL } from '../constants';
|
||||
|
||||
const EmployeeQuestionnaire: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const { user } = useAuth();
|
||||
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
|
||||
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) {
|
||||
loadInviteDetails(inviteCode);
|
||||
}
|
||||
}, [inviteCode]);
|
||||
|
||||
const loadInviteDetails = async (code: string) => {
|
||||
setIsLoadingInvite(true);
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/invitations/${code}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setInviteEmployee(data.employee);
|
||||
setError('');
|
||||
} else {
|
||||
setError('Invalid or expired invitation link');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load invitation details');
|
||||
} finally {
|
||||
setIsLoadingInvite(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get employee info from multiple sources
|
||||
const invitedEmployee = location.state?.invitedEmployee;
|
||||
|
||||
// Determine current employee - for invite flow, use invite employee data
|
||||
let currentEmployee;
|
||||
if (isInviteFlow) {
|
||||
currentEmployee = inviteEmployee;
|
||||
} else {
|
||||
// Original auth-based logic
|
||||
currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
||||
|
||||
// Additional matching strategies for edge cases
|
||||
if (!currentEmployee && user?.email) {
|
||||
// Try case-insensitive email matching
|
||||
currentEmployee = employees.find(emp =>
|
||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
||||
);
|
||||
|
||||
// Try matching by name if email doesn't work (for invite flow)
|
||||
if (!currentEmployee && invitedEmployee) {
|
||||
currentEmployee = employees.find(emp =>
|
||||
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no match by email, and we're in demo mode with only one recent employee, use that
|
||||
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
|
||||
// In demo mode, if there's only one employee or the most recent one, use it
|
||||
currentEmployee = employees[employees.length - 1];
|
||||
}
|
||||
|
||||
// If still no match and there's only one employee, assume it's them
|
||||
if (!currentEmployee && employees.length === 1) {
|
||||
currentEmployee = employees[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced debugging
|
||||
console.log('EmployeeQuestionnaire debug:', {
|
||||
userEmail: user?.email,
|
||||
employeesCount: employees.length,
|
||||
employeeEmails: employees.map(e => ({ id: e.id, email: e.email, name: e.name })),
|
||||
invitedEmployee,
|
||||
currentEmployee,
|
||||
locationState: location.state
|
||||
});
|
||||
|
||||
const handleAnswerChange = (questionId: string, value: string) => {
|
||||
setAnswers(prev => ({ ...prev, [questionId]: value }));
|
||||
};
|
||||
|
||||
// Filter out followup questions that shouldn't be shown yet
|
||||
const getVisibleQuestions = () => {
|
||||
return EMPLOYEE_QUESTIONS.filter(question => {
|
||||
// Hide follow-up questions since they're now integrated into the parent yes/no question
|
||||
if (question.followupTo) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFollowupChange = (questionId: string, value: string) => {
|
||||
setAnswers(prev => ({
|
||||
...prev,
|
||||
[questionId]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const submitViaInvite = async (employee: any, 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'
|
||||
});
|
||||
|
||||
if (!consumeResponse.ok) {
|
||||
throw new Error('Failed to process invitation');
|
||||
}
|
||||
|
||||
// Submit the questionnaire answers
|
||||
const submitResponse = await fetch(`${API_URL}/api/employee-submissions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
employeeId: employee.id,
|
||||
employee: employee,
|
||||
answers: answers
|
||||
})
|
||||
});
|
||||
|
||||
if (!submitResponse.ok) {
|
||||
throw new Error('Failed to submit questionnaire');
|
||||
}
|
||||
|
||||
const result = await submitResponse.json();
|
||||
return { success: true, reportGenerated: !!result.report };
|
||||
} catch (error) {
|
||||
console.error('Invite submission error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const visibleQuestions = getVisibleQuestions();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Validate required questions
|
||||
const requiredQuestions = visibleQuestions.filter(q => q.required);
|
||||
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
|
||||
|
||||
if (missingAnswers.length > 0) {
|
||||
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentEmployee) {
|
||||
// Enhanced fallback logic
|
||||
if (employees.length > 0) {
|
||||
// Try to find employee by matching with the user's email more aggressively
|
||||
let fallbackEmployee = employees.find(emp =>
|
||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
||||
);
|
||||
|
||||
// If still no match, use the most recent employee or one with matching domain
|
||||
if (!fallbackEmployee) {
|
||||
const userDomain = user?.email?.split('@')[1];
|
||||
fallbackEmployee = employees.find(emp =>
|
||||
emp.email?.split('@')[1] === userDomain
|
||||
) || employees[employees.length - 1];
|
||||
}
|
||||
|
||||
console.log('Using enhanced fallback employee:', fallbackEmployee);
|
||||
|
||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
||||
if (success) {
|
||||
// Generate LLM report for fallback employee
|
||||
console.log('Questionnaire submitted for fallback employee, generating report...');
|
||||
try {
|
||||
const report = await generateEmployeeReport(fallbackEmployee);
|
||||
if (report) {
|
||||
console.log('Report generated successfully for fallback employee:', report);
|
||||
}
|
||||
} catch (reportError) {
|
||||
console.error('Failed to generate report for fallback employee:', reportError);
|
||||
}
|
||||
|
||||
navigate('/questionnaire-complete', {
|
||||
replace: true,
|
||||
state: {
|
||||
employeeId: fallbackEmployee.id,
|
||||
employeeName: fallbackEmployee.name,
|
||||
message: 'Questionnaire submitted successfully! Your responses have been recorded.'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator to ensure your invite was set up correctly.`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Submit answers - different logic for invite vs auth flow
|
||||
let result;
|
||||
if (isInviteFlow) {
|
||||
// Direct API submission for invite flow (no auth needed)
|
||||
result = await submitViaInvite(currentEmployee, answers, inviteCode);
|
||||
} else {
|
||||
// Use org context for authenticated flow
|
||||
result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Show success message with AI report info
|
||||
const message = result.reportGenerated
|
||||
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
|
||||
: 'Questionnaire submitted successfully! Your report will be available shortly.';
|
||||
|
||||
setError(null);
|
||||
|
||||
// Navigate to completion page with success info
|
||||
navigate('/questionnaire-complete', {
|
||||
state: {
|
||||
employeeId: currentEmployee.id,
|
||||
employeeName: currentEmployee.name,
|
||||
reportGenerated: result.reportGenerated,
|
||||
message: message
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setError(result.message || 'Failed to submit questionnaire');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submission error:', error);
|
||||
setError('Failed to submit questionnaire. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getProgressPercentage = () => {
|
||||
const answeredQuestions = Object.keys(answers).filter(key => answers[key]?.trim()).length;
|
||||
return Math.round((answeredQuestions / visibleQuestions.length) * 100);
|
||||
};
|
||||
|
||||
// Early return for invite flow loading state
|
||||
if (isInviteFlow && isLoadingInvite) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Loading Your Invitation...</h1>
|
||||
<p className="text-[--text-secondary]">Please wait while we verify your invitation.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Early return for invite flow error state
|
||||
if (isInviteFlow && error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
!
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
||||
<Button onClick={() => window.location.href = '/'}>
|
||||
Return to Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
||||
Welcome to Auditly!
|
||||
</h1>
|
||||
<p className="text-[--text-secondary] mb-4">
|
||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
||||
</p>
|
||||
{currentEmployee ? (
|
||||
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
||||
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center px-4 py-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<span className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ Employee info not found. User: {user?.email}, Employees: {employees.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-[--text-secondary] max-w-md mx-auto">
|
||||
<p>Don't worry - your account was created successfully! This is likely a temporary sync issue.</p>
|
||||
<p className="mt-1">You can still complete the questionnaire, and we'll match it to your profile automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-[--text-secondary]">Progress</span>
|
||||
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
|
||||
</div>
|
||||
<LinearProgress value={getProgressPercentage()} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-6">
|
||||
{visibleQuestions.map((question, index) => (
|
||||
<Question
|
||||
key={question.id}
|
||||
label={`${index + 1}. ${question.prompt}`}
|
||||
required={question.required}
|
||||
description={`Category: ${question.category}`}
|
||||
>
|
||||
<QuestionInput
|
||||
question={question}
|
||||
value={answers[question.id] || ''}
|
||||
onChange={(value) => handleAnswerChange(question.id, value)}
|
||||
allAnswers={answers}
|
||||
onFollowupChange={handleFollowupChange}
|
||||
/>
|
||||
</Question>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-6">
|
||||
<Alert variant="error" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-center">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || getProgressPercentage() < 70}
|
||||
className="px-8 py-3"
|
||||
>
|
||||
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{getProgressPercentage() < 70 && (
|
||||
<p className="text-center text-sm text-[--text-secondary] mt-4">
|
||||
Please answer at least 70% of the questions to submit.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeQuestionnaire;
|
||||
381
pages/EmployeeQuestionnaireSteps.tsx
Normal file
381
pages/EmployeeQuestionnaireSteps.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
|
||||
import { Question } from '../components/ui/Question';
|
||||
import { QuestionInput } from '../components/ui/QuestionInput';
|
||||
import { FigmaQuestion } from '../components/figma/FigmaQuestion';
|
||||
import { EnhancedFigmaQuestion } from '../components/figma/EnhancedFigmaQuestion';
|
||||
import { LinearProgress } from '../components/ui/Progress';
|
||||
import { Alert } from '../components/ui/Alert';
|
||||
|
||||
const EmployeeQuestionnaireSteps: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
|
||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Get employee info from multiple sources
|
||||
const invitedEmployee = location.state?.invitedEmployee;
|
||||
|
||||
// Find current employee info - try multiple strategies
|
||||
let currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
||||
|
||||
// Additional matching strategies for edge cases
|
||||
if (!currentEmployee && user?.email) {
|
||||
// Try case-insensitive email matching
|
||||
currentEmployee = employees.find(emp =>
|
||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
||||
);
|
||||
|
||||
// Try matching by name if email doesn't work (for invite flow)
|
||||
if (!currentEmployee && invitedEmployee) {
|
||||
currentEmployee = employees.find(emp =>
|
||||
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Demo mode fallbacks
|
||||
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
|
||||
currentEmployee = employees[employees.length - 1];
|
||||
}
|
||||
if (!currentEmployee && employees.length === 1) {
|
||||
currentEmployee = employees[0];
|
||||
}
|
||||
|
||||
// Filter out followup questions that shouldn't be shown yet
|
||||
const getVisibleQuestions = () => {
|
||||
return EMPLOYEE_QUESTIONS.filter(question => {
|
||||
if (!question.followupTo) return true;
|
||||
|
||||
const parentAnswer = answers[question.followupTo];
|
||||
if (question.followupTo === 'has_kpis') {
|
||||
return parentAnswer === 'Yes';
|
||||
}
|
||||
if (question.followupTo === 'unclear_responsibilities') {
|
||||
return parentAnswer === 'Yes';
|
||||
}
|
||||
if (question.followupTo === 'role_shift_interest') {
|
||||
return parentAnswer === 'Yes';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const visibleQuestions = getVisibleQuestions();
|
||||
const currentQuestion = visibleQuestions[currentStep];
|
||||
|
||||
const handleAnswerChange = (value: string) => {
|
||||
if (currentQuestion) {
|
||||
setAnswers(prev => ({ ...prev, [currentQuestion.id]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < visibleQuestions.length - 1) {
|
||||
setCurrentStep(prev => prev + 1);
|
||||
} else {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(prev => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Validate required questions
|
||||
const requiredQuestions = visibleQuestions.filter(q => q.required);
|
||||
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
|
||||
|
||||
if (missingAnswers.length > 0) {
|
||||
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentEmployee) {
|
||||
// Enhanced fallback logic
|
||||
if (employees.length > 0) {
|
||||
// Try to find employee by matching with the user's email more aggressively
|
||||
let fallbackEmployee = employees.find(emp =>
|
||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
||||
);
|
||||
|
||||
// If still no match, use the most recent employee or one with matching domain
|
||||
if (!fallbackEmployee) {
|
||||
const userDomain = user?.email?.split('@')[1];
|
||||
fallbackEmployee = employees.find(emp =>
|
||||
emp.email?.split('@')[1] === userDomain
|
||||
) || employees[employees.length - 1];
|
||||
}
|
||||
|
||||
console.log('Using enhanced fallback employee:', fallbackEmployee);
|
||||
|
||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
||||
if (success) {
|
||||
try {
|
||||
const report = await generateEmployeeReport(fallbackEmployee);
|
||||
if (report) {
|
||||
console.log('Report generated successfully for fallback employee:', report);
|
||||
}
|
||||
} catch (reportError) {
|
||||
console.error('Failed to generate report for fallback employee:', reportError);
|
||||
}
|
||||
navigate('/questionnaire-complete', {
|
||||
replace: true,
|
||||
state: {
|
||||
employeeId: fallbackEmployee.id,
|
||||
employeeName: fallbackEmployee.name,
|
||||
message: 'Questionnaire submitted successfully! Your responses have been recorded.'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator to ensure your invite was set up correctly.`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
||||
|
||||
if (result.success) {
|
||||
const message = result.reportGenerated
|
||||
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
|
||||
: 'Questionnaire submitted successfully! Your report will be available shortly.';
|
||||
|
||||
navigate('/questionnaire-complete', {
|
||||
state: {
|
||||
employeeId: currentEmployee.id,
|
||||
employeeName: currentEmployee.name,
|
||||
reportGenerated: result.reportGenerated,
|
||||
message: message
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setError(result.message || 'Failed to submit questionnaire');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submission error:', error);
|
||||
setError('Failed to submit questionnaire. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getProgressPercentage = () => {
|
||||
return Math.round(((currentStep + 1) / visibleQuestions.length) * 100);
|
||||
};
|
||||
|
||||
const isCurrentQuestionAnswered = () => {
|
||||
if (!currentQuestion) return false;
|
||||
const answer = answers[currentQuestion.id];
|
||||
return answer && answer.trim().length > 0;
|
||||
};
|
||||
|
||||
if (!currentQuestion) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
|
||||
No questions available
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
||||
Welcome to Auditly!
|
||||
</h1>
|
||||
<p className="text-[--text-secondary] mb-4">
|
||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
||||
</p>
|
||||
{currentEmployee ? (
|
||||
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
||||
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center px-4 py-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<span className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ Employee info not found. User: {user?.email}, Employees: {employees.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-[--text-secondary] max-w-md mx-auto">
|
||||
<p>Don't worry - your account was created successfully! This is likely a temporary sync issue.</p>
|
||||
<p className="mt-1">You can still complete the questionnaire, and we'll match it to your profile automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-[--text-secondary]">
|
||||
Question {currentStep + 1} of {visibleQuestions.length}
|
||||
</span>
|
||||
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
|
||||
</div>
|
||||
<LinearProgress value={getProgressPercentage()} />
|
||||
</div>
|
||||
|
||||
{/* Enhanced Question Card */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="w-full max-w-2xl">
|
||||
<EnhancedFigmaQuestion
|
||||
questionNumber={`Q${currentStep + 1}`}
|
||||
question={currentQuestion}
|
||||
answer={answers[currentQuestion.id] || ''}
|
||||
onAnswerChange={handleAnswerChange}
|
||||
onBack={currentStep > 0 ? handleBack : undefined}
|
||||
onNext={
|
||||
currentStep < visibleQuestions.length - 1
|
||||
? handleNext
|
||||
: (isCurrentQuestionAnswered() || !currentQuestion.required)
|
||||
? handleSubmit
|
||||
: undefined
|
||||
}
|
||||
nextLabel={
|
||||
currentStep < visibleQuestions.length - 1
|
||||
? 'Next'
|
||||
: isSubmitting
|
||||
? 'Submitting...'
|
||||
: 'Submit & Generate Report'
|
||||
}
|
||||
showNavigation={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Original Figma Question Card for Comparison */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="w-full max-w-2xl">
|
||||
<FigmaQuestion
|
||||
questionNumber={`Q${currentStep + 1}`}
|
||||
title={currentQuestion.prompt}
|
||||
description={currentQuestion.required ? 'Required' : 'Optional'}
|
||||
answer={answers[currentQuestion.id] || ''}
|
||||
onAnswerChange={handleAnswerChange}
|
||||
onBack={currentStep > 0 ? handleBack : undefined}
|
||||
onNext={
|
||||
currentStep < visibleQuestions.length - 1
|
||||
? handleNext
|
||||
: (isCurrentQuestionAnswered() || !currentQuestion.required)
|
||||
? handleSubmit
|
||||
: undefined
|
||||
}
|
||||
nextLabel={
|
||||
currentStep < visibleQuestions.length - 1
|
||||
? 'Next'
|
||||
: isSubmitting
|
||||
? 'Submitting...'
|
||||
: 'Submit & Generate Report'
|
||||
}
|
||||
showNavigation={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alternative Input for Different Question Types */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="w-full max-w-2xl">
|
||||
<Card className="p-6">
|
||||
<Question
|
||||
label={`${currentStep + 1}. ${currentQuestion.prompt}`}
|
||||
required={currentQuestion.required}
|
||||
description={`Category: ${currentQuestion.category}`}
|
||||
>
|
||||
<QuestionInput
|
||||
question={currentQuestion}
|
||||
value={answers[currentQuestion.id] || ''}
|
||||
onChange={handleAnswerChange}
|
||||
/>
|
||||
</Question>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="w-full max-w-2xl">
|
||||
<Alert variant="error" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-center gap-4">
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep < visibleQuestions.length - 1 ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={currentQuestion.required && !isCurrentQuestionAnswered()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(currentQuestion.required && !isCurrentQuestionAnswered())
|
||||
}
|
||||
>
|
||||
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-sm text-[--text-secondary]">
|
||||
{currentQuestion.required && !isCurrentQuestionAnswered()
|
||||
? 'Please answer this required question to continue.'
|
||||
: 'You can skip optional questions or come back to them later.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeQuestionnaireSteps;
|
||||
132
pages/FormsDashboard.tsx
Normal file
132
pages/FormsDashboard.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { Card } from '../components/UiKit';
|
||||
|
||||
const FormsDashboard: React.FC = () => {
|
||||
const forms = [
|
||||
{
|
||||
title: 'Question Types Demo',
|
||||
description: 'Interactive showcase of all question types and input styles',
|
||||
url: '#/question-types-demo',
|
||||
color: 'bg-purple-500',
|
||||
features: ['All Question Types', 'Side-by-side Comparison', 'Live Preview']
|
||||
},
|
||||
{
|
||||
title: 'Traditional Questionnaire',
|
||||
description: 'Complete questionnaire on a single page with all questions visible',
|
||||
url: '#/employee-questionnaire',
|
||||
color: 'bg-blue-500',
|
||||
features: ['All Questions Visible', 'Scroll Navigation', 'Conditional Logic']
|
||||
},
|
||||
{
|
||||
title: 'Stepped Questionnaire',
|
||||
description: 'One question at a time with progress tracking and navigation',
|
||||
url: '#/employee-questionnaire-steps',
|
||||
color: 'bg-green-500',
|
||||
features: ['One Question Per Step', 'Progress Tracking', 'Back/Next Navigation']
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
📝
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
||||
Employee Forms Dashboard
|
||||
</h1>
|
||||
<p className="text-[--text-secondary] max-w-2xl mx-auto">
|
||||
Explore the different employee questionnaire implementations.
|
||||
Each form demonstrates different UX approaches and question types based on the Figma designs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
{forms.map((form, index) => (
|
||||
<Card key={index} className="p-6 hover:shadow-lg transition-shadow">
|
||||
<div className={`w-12 h-12 ${form.color} rounded-lg flex items-center justify-center text-white font-bold text-xl mb-4`}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[--text-primary] mb-2">
|
||||
{form.title}
|
||||
</h3>
|
||||
<p className="text-[--text-secondary] mb-4 text-sm">
|
||||
{form.description}
|
||||
</p>
|
||||
<div className="space-y-2 mb-4">
|
||||
{form.features.map((feature, featureIndex) => (
|
||||
<div key={featureIndex} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="text-sm text-[--text-secondary]">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href={form.url}
|
||||
className={`w-full inline-block text-center px-4 py-2 ${form.color} text-white rounded-lg hover:opacity-90 transition-opacity`}
|
||||
>
|
||||
Try This Form
|
||||
</a>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Technical Overview */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-2xl font-semibold text-[--text-primary] mb-4">
|
||||
Implementation Overview
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">
|
||||
Question Types Implemented
|
||||
</h3>
|
||||
<ul className="space-y-2 text-[--text-secondary]">
|
||||
<li>• <strong>Text Input:</strong> Single-line text for names, titles</li>
|
||||
<li>• <strong>Textarea:</strong> Multi-line for detailed responses</li>
|
||||
<li>• <strong>Scale Rating:</strong> 1-10 sliders with custom labels</li>
|
||||
<li>• <strong>Yes/No Radio:</strong> Binary choice questions</li>
|
||||
<li>• <strong>Select Dropdown:</strong> Predefined option selection</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-3">
|
||||
Key Features
|
||||
</h3>
|
||||
<ul className="space-y-2 text-[--text-secondary]">
|
||||
<li>• <strong>Conditional Logic:</strong> Follow-up questions based on answers</li>
|
||||
<li>• <strong>Progress Tracking:</strong> Visual completion indicators</li>
|
||||
<li>• <strong>Responsive Design:</strong> Mobile-friendly layouts</li>
|
||||
<li>• <strong>Figma Integration:</strong> Design system compliance</li>
|
||||
<li>• <strong>Theme Support:</strong> Light/dark mode compatibility</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
||||
<Card className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-blue-500">32</div>
|
||||
<div className="text-sm text-[--text-secondary]">Total Questions</div>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-500">5</div>
|
||||
<div className="text-sm text-[--text-secondary]">Question Types</div>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-purple-500">8</div>
|
||||
<div className="text-sm text-[--text-secondary]">Categories</div>
|
||||
</Card>
|
||||
<Card className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-orange-500">3</div>
|
||||
<div className="text-sm text-[--text-secondary]">Conditional Flows</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormsDashboard;
|
||||
335
pages/HelpAndSettings.tsx
Normal file
335
pages/HelpAndSettings.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
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(`Invitation sent! Share this link: ${result.inviteLink}`);
|
||||
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 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>
|
||||
)}
|
||||
|
||||
<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;
|
||||
266
pages/Login.tsx
Normal file
266
pages/Login.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { inviteCode: routeInviteCode } = useParams<{ inviteCode: string }>();
|
||||
const [email, setEmail] = useState('demo@auditly.com');
|
||||
const [password, setPassword] = useState('demo123');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState<string | null>(null);
|
||||
const { signInWithGoogle, signInWithEmail, signUpWithEmail, user, loading } = useAuth();
|
||||
const { consumeInvite, org } = useOrg();
|
||||
|
||||
useEffect(() => {
|
||||
// Check for invite code in route params first, then fallback to query params
|
||||
if (routeInviteCode) {
|
||||
console.log('Invite code from route params:', routeInviteCode);
|
||||
setInviteCode(routeInviteCode);
|
||||
// Clear demo credentials for invite flow
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
} else {
|
||||
// Extract query params from hash-based URL
|
||||
const hashSearch = location.hash.includes('?') ? location.hash.split('?')[1] : '';
|
||||
const searchParams = new URLSearchParams(hashSearch);
|
||||
const queryInvite = searchParams.get('invite');
|
||||
if (queryInvite) {
|
||||
console.log('Invite code from query params:', queryInvite);
|
||||
setInviteCode(queryInvite);
|
||||
// Clear demo credentials for invite flow
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
}
|
||||
}
|
||||
}, [routeInviteCode, location]);
|
||||
|
||||
const handleSuccessfulLogin = async () => {
|
||||
if (inviteCode) {
|
||||
// Invite flow - redirect to org selection with invite code
|
||||
navigate(`/org-selection?invite=${inviteCode}`, { replace: true });
|
||||
} else {
|
||||
// Regular login - redirect to org selection to choose/create org
|
||||
navigate('/org-selection', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !loading) {
|
||||
handleSuccessfulLogin();
|
||||
}
|
||||
}, [user, loading]);
|
||||
|
||||
const handleEmailLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
if (inviteCode) {
|
||||
// For invites, try to create account first since they're new users
|
||||
console.log('Invite flow: attempting to create account for', email);
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
} else {
|
||||
// Regular login
|
||||
await signInWithEmail(email, password);
|
||||
}
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (error) {
|
||||
console.error('Auth failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
if (inviteCode) {
|
||||
// For invite flow, if account creation failed, try login instead
|
||||
if (errorMessage.includes('User already exists') || errorMessage.includes('already-exists')) {
|
||||
try {
|
||||
console.log('Account exists, trying login instead...');
|
||||
await signInWithEmail(email, password);
|
||||
} catch (loginError) {
|
||||
console.error('Login also failed:', loginError);
|
||||
setError(`Account exists but password is incorrect. Please check your password or contact your administrator.`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setError(`Failed to create account: ${errorMessage}. Please try a different email or contact your administrator.`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
// Regular login flow - try signup if user not found
|
||||
if (errorMessage.includes('User not found')) {
|
||||
try {
|
||||
console.log('User not found, attempting sign-up...');
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (signUpError) {
|
||||
console.error('Sign-up also failed:', signUpError);
|
||||
setError(`Failed to create account: ${signUpError instanceof Error ? signUpError.message : 'Unknown error'}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setError(`Login failed: ${errorMessage}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (error) {
|
||||
console.error('Google login failed:', error);
|
||||
setError(`Google login failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[--background-primary] py-12 px-4 sm:px-6 lg:px-8">
|
||||
<Card className="max-w-md w-full space-y-8" padding="lg">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[--text-primary]">Welcome to Auditly</h2>
|
||||
<p className="text-[--text-secondary] mt-2">
|
||||
{inviteCode ? 'Complete your profile to join the team survey' : 'Sign in to your account'}
|
||||
</p>
|
||||
{inviteCode && (
|
||||
<div className="mt-3 p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<EFBFBD> <strong>Employee Survey Invitation</strong><br />
|
||||
No account needed! Just create a password to secure your responses and start the questionnaire.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-3 p-3 bg-red-100 dark:bg-red-900 rounded-lg">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEmailLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Email {inviteCode && <span className="text-gray-500 dark:text-gray-400">(use your work email)</span>}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password {inviteCode && <span className="text-gray-500 dark:text-gray-400">(create a new password)</span>}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
|
||||
required
|
||||
/>
|
||||
{inviteCode && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Choose a secure password for your new account
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Processing...' : (inviteCode ? 'Create Account & Join Team' : 'Sign In')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">Or continue with</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Manual invite code entry - only show if no invite code in URL */}
|
||||
{!inviteCode && (
|
||||
<div className="border-t border-[--border-color] pt-6">
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-sm font-medium text-[--text-primary] mb-2">Employee? Use Your Invite Code</h3>
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
Skip account creation - employees can go directly to their questionnaire
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your invite code"
|
||||
className="flex-1 px-3 py-2 border border-[--input-border] rounded-lg bg-[--input-bg] text-[--text-primary] placeholder-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const code = (e.target as HTMLInputElement).value.trim();
|
||||
if (code) {
|
||||
window.location.href = `#/invite/${code}`;
|
||||
} else {
|
||||
alert('Please enter an invite code');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector('input[placeholder="Enter your invite code"]') as HTMLInputElement;
|
||||
const code = input?.value.trim();
|
||||
if (code) {
|
||||
window.location.href = `#/invite/${code}`;
|
||||
} else {
|
||||
alert('Please enter an invite code');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Start Survey
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-[--text-secondary] mt-2 text-center">
|
||||
No account needed - just answer questions and submit
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
{inviteCode ?
|
||||
'Demo mode: Enter any email and password to create your account.' :
|
||||
'Demo mode: No Firebase configuration detected.\nUse any email/password to continue.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
403
pages/ModernLogin.tsx
Normal file
403
pages/ModernLogin.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Button } from '../components/UiKit';
|
||||
|
||||
type AuthStep = 'email' | 'otp' | 'password-fallback';
|
||||
|
||||
const ModernLogin: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { inviteCode: routeInviteCode } = useParams<{ inviteCode: string }>();
|
||||
|
||||
// Auth state
|
||||
const { signInWithGoogle, signInWithEmail, signUpWithEmail, user, loading, sendOTP: authSendOTP, verifyOTP: authVerifyOTP } = useAuth();
|
||||
|
||||
// Form state
|
||||
const [step, setStep] = useState<AuthStep>('email');
|
||||
const [email, setEmail] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState<string | null>(null);
|
||||
const [resendCooldown, setResendCooldown] = useState(0);
|
||||
const [demoOTP, setDemoOTP] = useState<string | null>(null);
|
||||
|
||||
// Extract invite code from URL
|
||||
useEffect(() => {
|
||||
if (routeInviteCode) {
|
||||
setInviteCode(routeInviteCode);
|
||||
} else {
|
||||
const hashSearch = location.hash.includes('?') ? location.hash.split('?')[1] : '';
|
||||
const searchParams = new URLSearchParams(hashSearch);
|
||||
const queryInvite = searchParams.get('invite');
|
||||
if (queryInvite) {
|
||||
setInviteCode(queryInvite);
|
||||
}
|
||||
}
|
||||
}, [routeInviteCode, location]);
|
||||
|
||||
// Handle successful authentication
|
||||
useEffect(() => {
|
||||
if (user && !loading) {
|
||||
if (inviteCode) {
|
||||
navigate(`/org-selection?invite=${inviteCode}`, { replace: true });
|
||||
} else {
|
||||
navigate('/org-selection', { replace: true });
|
||||
}
|
||||
}
|
||||
}, [user, loading, navigate, inviteCode]);
|
||||
|
||||
// Resend cooldown timer
|
||||
useEffect(() => {
|
||||
if (resendCooldown > 0) {
|
||||
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendCooldown]);
|
||||
|
||||
const sendOTP = async (emailAddress: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setDemoOTP(null);
|
||||
|
||||
// Call auth context method
|
||||
const response = await authSendOTP(emailAddress, inviteCode || undefined);
|
||||
|
||||
// If OTP is returned in response (demo mode), display it
|
||||
if (response.otp) {
|
||||
setDemoOTP(response.otp);
|
||||
}
|
||||
|
||||
setStep('otp');
|
||||
setResendCooldown(60); // 60 second cooldown
|
||||
|
||||
} catch (err) {
|
||||
console.error('OTP send error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to send verification code. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}; const verifyOTP = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Call auth context method
|
||||
await authVerifyOTP(email, otp, inviteCode || undefined);
|
||||
|
||||
// Success - user will be set in auth context and useEffect will handle navigation
|
||||
|
||||
} catch (err) {
|
||||
console.error('OTP verification error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Invalid verification code. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email.trim()) return;
|
||||
|
||||
await sendOTP(email);
|
||||
};
|
||||
|
||||
const handleOTPSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!otp.trim()) return;
|
||||
|
||||
await verifyOTP();
|
||||
};
|
||||
|
||||
const handlePasswordFallback = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!password.trim()) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Try login first, then signup if user doesn't exist
|
||||
try {
|
||||
await signInWithEmail(email, password);
|
||||
} catch (loginError) {
|
||||
// If login fails, try creating account
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Password auth error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Authentication failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleAuth = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
await signInWithGoogle();
|
||||
} catch (err) {
|
||||
console.error('Google auth error:', err);
|
||||
setError('Google authentication failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmailStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-white text-2xl font-bold">A</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{inviteCode ? 'Join Organization' : 'Welcome to Auditly'}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
{inviteCode
|
||||
? 'Enter your email to join the organization'
|
||||
: 'Enter your email to get started'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEmailSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66675 5.83325L8.47085 10.5961C9.02182 10.9818 9.29731 11.1746 9.59697 11.2493C9.86166 11.3153 10.1385 11.3153 10.4032 11.2493C10.7029 11.1746 10.9783 10.9818 11.5293 10.5961L18.3334 5.83325M5.66675 16.6666H14.3334C15.7335 16.6666 16.4336 16.6666 16.9684 16.3941C17.4388 16.1544 17.8212 15.772 18.0609 15.3016C18.3334 14.7668 18.3334 14.0667 18.3334 12.6666V7.33325C18.3334 5.93312 18.3334 5.23306 18.0609 4.69828C17.8212 4.22787 17.4388 3.84542 16.9684 3.60574C16.4336 3.33325 15.7335 3.33325 14.3334 3.33325H5.66675C4.26662 3.33325 3.56655 3.33325 3.03177 3.60574C2.56137 3.84542 2.17892 4.22787 1.93923 4.69828C1.66675 5.23306 1.66675 5.93312 1.66675 7.33325V12.6666C1.66675 14.0667 1.66675 14.7668 1.93923 15.3016C2.17892 15.772 2.56137 16.1544 3.03177 16.3941C3.56655 16.6666 4.26662 16.6666 5.66675 16.6666Z" stroke="#718096" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-gray-50 border text-gray-700 border-gray-200 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3.5 rounded-full font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || !email.trim()}
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Continue with Email'}
|
||||
</Button>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-3 bg-white text-gray-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full border-gray-200 py-3.5 rounded-full transition-colors"
|
||||
onClick={handleGoogleAuth}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep('password-fallback')}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Use password instead
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderOTPStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Check your email</h1>
|
||||
<p className="text-gray-600">
|
||||
We sent a verification code to <br />
|
||||
<strong className="text-gray-900">{email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleOTPSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2 text-center">
|
||||
Verification Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
placeholder="000000"
|
||||
className="w-full px-4 py-4 bg-gray-50 border border-gray-200 text-gray-700 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-center text-3xl tracking-[0.5em] font-mono outline-none transition-all"
|
||||
maxLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{demoOTP && (
|
||||
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 border border-yellow-200 rounded-xl p-4">
|
||||
<div className="text-yellow-800 text-sm text-center">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<strong>Demo Mode</strong>
|
||||
</div>
|
||||
Your verification code is <strong className="text-2xl font-mono bg-yellow-100 px-2 py-1 rounded">{demoOTP}</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-4 rounded-xl font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || otp.length !== 6}
|
||||
>
|
||||
{isLoading ? 'Verifying...' : 'Verify Code'}
|
||||
</Button>
|
||||
|
||||
<div className="text-center space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendOTP(email)}
|
||||
disabled={resendCooldown > 0 || isLoading}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 disabled:text-gray-400 transition-colors"
|
||||
>
|
||||
{resendCooldown > 0
|
||||
? `Resend code in ${resendCooldown}s`
|
||||
: 'Resend code'
|
||||
}
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep('email'); setError(''); setOtp(''); }}
|
||||
className="text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||
>
|
||||
← Change email address
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderPasswordStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Sign in with password</h1>
|
||||
<p className="text-gray-600">
|
||||
Enter your password for <br />
|
||||
<strong className="text-gray-900">{email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handlePasswordFallback} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3.5 rounded-full font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || !password.trim()}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep('email'); setError(''); setPassword(''); }}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
← Back to email verification
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 flex items-center justify-center p-4">
|
||||
{step === 'email' && renderEmailStep()}
|
||||
{step === 'otp' && renderOTPStep()}
|
||||
{step === 'password-fallback' && renderPasswordStep()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModernLogin;
|
||||
719
pages/Onboarding.tsx
Normal file
719
pages/Onboarding.tsx
Normal file
@@ -0,0 +1,719 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { FigmaProgress } from '../components/figma/FigmaProgress';
|
||||
import { FigmaInput } from '../components/figma/FigmaInput';
|
||||
import { FigmaAlert } from '../components/figma/FigmaAlert';
|
||||
|
||||
interface OnboardingData {
|
||||
// Step 1: Company Basics
|
||||
companyName: string;
|
||||
industry: string;
|
||||
size: string;
|
||||
description: string;
|
||||
|
||||
// Step 2: Mission & Vision
|
||||
mission: string;
|
||||
vision: string;
|
||||
values: string[];
|
||||
|
||||
// Step 3: Company Evolution & History
|
||||
foundingYear: string;
|
||||
evolution: string;
|
||||
majorMilestones: string;
|
||||
|
||||
// Step 4: Competitive Landscape
|
||||
advantages: string;
|
||||
vulnerabilities: string;
|
||||
competitors: string;
|
||||
marketPosition: string;
|
||||
|
||||
// Step 5: Current Challenges & Goals
|
||||
currentChallenges: string[];
|
||||
shortTermGoals: string;
|
||||
longTermGoals: string;
|
||||
keyMetrics: string;
|
||||
|
||||
// Step 6: Team & Culture
|
||||
cultureDescription: string;
|
||||
workEnvironment: string;
|
||||
leadershipStyle: string;
|
||||
communicationStyle: string;
|
||||
|
||||
// Step 7: Final Review
|
||||
additionalContext: string;
|
||||
}
|
||||
|
||||
const Onboarding: React.FC = () => {
|
||||
const { org, upsertOrg, generateCompanyWiki } = useOrg();
|
||||
const navigate = useNavigate();
|
||||
const [step, setStep] = useState(0);
|
||||
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
|
||||
const [formData, setFormData] = useState<OnboardingData>({
|
||||
companyName: org?.name || '',
|
||||
industry: '',
|
||||
size: '',
|
||||
description: '',
|
||||
mission: '',
|
||||
vision: '',
|
||||
values: [],
|
||||
foundingYear: '',
|
||||
evolution: '',
|
||||
majorMilestones: '',
|
||||
advantages: '',
|
||||
vulnerabilities: '',
|
||||
competitors: '',
|
||||
marketPosition: '',
|
||||
currentChallenges: [],
|
||||
shortTermGoals: '',
|
||||
longTermGoals: '',
|
||||
keyMetrics: '',
|
||||
cultureDescription: '',
|
||||
workEnvironment: '',
|
||||
leadershipStyle: '',
|
||||
communicationStyle: '',
|
||||
additionalContext: ''
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Company Basics',
|
||||
description: 'Tell us about your company fundamentals'
|
||||
},
|
||||
{
|
||||
title: 'Mission & Vision',
|
||||
description: 'Define your purpose and direction'
|
||||
},
|
||||
{
|
||||
title: 'Evolution & History',
|
||||
description: 'Share your company\'s journey'
|
||||
},
|
||||
{
|
||||
title: 'Competitive Position',
|
||||
description: 'Understand your market position'
|
||||
},
|
||||
{
|
||||
title: 'Goals & Challenges',
|
||||
description: 'Current objectives and obstacles'
|
||||
},
|
||||
{
|
||||
title: 'Team & Culture',
|
||||
description: 'Describe your work environment'
|
||||
},
|
||||
{
|
||||
title: 'Final Review',
|
||||
description: 'Complete your company profile'
|
||||
}
|
||||
];
|
||||
|
||||
const handleNext = async () => {
|
||||
// Prevent re-entry during generation
|
||||
if (isGeneratingReport) return;
|
||||
|
||||
if (step < steps.length - 1) {
|
||||
setStep(prev => prev + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Final step: persist org & generate report
|
||||
setIsGeneratingReport(true);
|
||||
console.log('Starting onboarding completion...', { step });
|
||||
try {
|
||||
const newOrgData = {
|
||||
name: formData.companyName,
|
||||
industry: formData.industry,
|
||||
size: formData.size,
|
||||
description: formData.description,
|
||||
mission: formData.mission,
|
||||
vision: formData.vision,
|
||||
values: formData.values.join(','),
|
||||
foundingYear: formData.foundingYear,
|
||||
evolution: formData.evolution,
|
||||
majorMilestones: formData.majorMilestones,
|
||||
advantages: formData.advantages,
|
||||
vulnerabilities: formData.vulnerabilities,
|
||||
competitors: formData.competitors,
|
||||
marketPosition: formData.marketPosition,
|
||||
currentChallenges: formData.currentChallenges.join(','),
|
||||
shortTermGoals: formData.shortTermGoals,
|
||||
longTermGoals: formData.longTermGoals,
|
||||
keyMetrics: formData.keyMetrics,
|
||||
cultureDescription: formData.cultureDescription,
|
||||
workEnvironment: formData.workEnvironment,
|
||||
leadershipStyle: formData.leadershipStyle,
|
||||
communicationStyle: formData.communicationStyle,
|
||||
additionalContext: formData.additionalContext,
|
||||
onboardingCompleted: true
|
||||
};
|
||||
|
||||
console.log('Saving org data...', newOrgData);
|
||||
await upsertOrg(newOrgData);
|
||||
console.log('Org data saved successfully');
|
||||
|
||||
console.log('Generating company wiki...');
|
||||
await generateCompanyWiki(newOrgData);
|
||||
console.log('Company wiki generated successfully');
|
||||
|
||||
// Small delay to ensure states are updated, then redirect
|
||||
console.log('Redirecting to reports...');
|
||||
setTimeout(() => {
|
||||
console.log('Navigation executing...');
|
||||
navigate('/reports', { replace: true });
|
||||
console.log('Navigation called successfully');
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('Error completing onboarding:', error);
|
||||
// Show detailed error to user for debugging
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
alert(`There was an error completing the setup: ${errorMessage}. Please check the console for more details and try again.`);
|
||||
} finally {
|
||||
setIsGeneratingReport(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step > 0) setStep(step - 1);
|
||||
};
|
||||
|
||||
const addToArray = (field: 'values' | 'currentChallenges', value: string) => {
|
||||
if (value.trim()) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: [...prev[field], value.trim()]
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromArray = (field: 'values' | 'currentChallenges', index: number) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: prev[field].filter((_, i) => i !== index)
|
||||
}));
|
||||
};
|
||||
|
||||
const renderStep = () => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Company Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.companyName}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, companyName: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter your company name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Industry *
|
||||
</label>
|
||||
<select
|
||||
value={formData.industry}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, industry: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select industry</option>
|
||||
<option value="Technology">Technology</option>
|
||||
<option value="Healthcare">Healthcare</option>
|
||||
<option value="Finance">Finance</option>
|
||||
<option value="Manufacturing">Manufacturing</option>
|
||||
<option value="Retail">Retail</option>
|
||||
<option value="Professional Services">Professional Services</option>
|
||||
<option value="Education">Education</option>
|
||||
<option value="Media & Entertainment">Media & Entertainment</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Company Size *
|
||||
</label>
|
||||
<select
|
||||
value={formData.size}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, size: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select size</option>
|
||||
<option value="1-10">1-10 employees</option>
|
||||
<option value="11-50">11-50 employees</option>
|
||||
<option value="51-200">51-200 employees</option>
|
||||
<option value="201-1000">201-1000 employees</option>
|
||||
<option value="1000+">1000+ employees</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Company Description *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Describe what your company does, its products/services, and target market"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Mission Statement *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.mission}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, mission: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="What is your company's purpose? Why does it exist?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Vision Statement *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.vision}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, vision: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="Where do you see your company in the future? What impact do you want to make?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Core Values
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{formData.values.map((value, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<span className="flex-1 px-3 py-2 bg-[--background-tertiary] rounded-lg text-[--text-primary]">
|
||||
{value}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => removeFromArray('values', index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Add a core value"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addToArray('values', e.currentTarget.value);
|
||||
e.currentTarget.value = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
const input = (e.target as HTMLElement).parentElement?.querySelector('input');
|
||||
if (input) {
|
||||
addToArray('values', input.value);
|
||||
input.value = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Founding Year
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.foundingYear}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, foundingYear: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="When was your company founded?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Company Evolution *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.evolution}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, evolution: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="How has your company evolved since its founding? What major changes or pivots have occurred?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Major Milestones
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.majorMilestones}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, majorMilestones: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="List key achievements, product launches, funding rounds, or other significant milestones"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Competitive Advantages *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.advantages}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, advantages: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="What gives your company a competitive edge? What are your unique strengths?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Vulnerabilities & Weaknesses *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.vulnerabilities}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, vulnerabilities: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="What are your company's current weaknesses or areas of vulnerability?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Key Competitors
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.competitors}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, competitors: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="Who are your main competitors? How do you differentiate from them?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Market Position
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.marketPosition}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, marketPosition: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="How do you position yourself in the market? What's your market share or standing?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 4:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Current Challenges
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{formData.currentChallenges.map((challenge, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<span className="flex-1 px-3 py-2 bg-[--background-tertiary] rounded-lg text-[--text-primary]">
|
||||
{challenge}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => removeFromArray('currentChallenges', index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Add a current challenge"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addToArray('currentChallenges', e.currentTarget.value);
|
||||
e.currentTarget.value = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
const input = (e.target as HTMLElement).parentElement?.querySelector('input');
|
||||
if (input) {
|
||||
addToArray('currentChallenges', input.value);
|
||||
input.value = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Short-term Goals (6-12 months) *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.shortTermGoals}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, shortTermGoals: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="What are your immediate priorities and goals for the next 6-12 months?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Long-term Goals (1-3 years) *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.longTermGoals}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, longTermGoals: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="What are your strategic objectives for the next 1-3 years?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Key Metrics & Success Indicators
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.keyMetrics}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, keyMetrics: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="How do you measure success? What are your key performance indicators?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 5:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Company Culture *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.cultureDescription}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, cultureDescription: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Describe your company culture. What's it like to work at your company?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Work Environment *
|
||||
</label>
|
||||
<select
|
||||
value={formData.workEnvironment}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, workEnvironment: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select work environment</option>
|
||||
<option value="Remote">Fully Remote</option>
|
||||
<option value="Hybrid">Hybrid (Remote + Office)</option>
|
||||
<option value="In-office">In-office</option>
|
||||
<option value="Flexible">Flexible/Varies by role</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Leadership Style *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.leadershipStyle}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, leadershipStyle: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="Describe the leadership approach and management style in your organization"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Communication Style *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.communicationStyle}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, communicationStyle: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="How does your team communicate? What tools and processes do you use?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 6:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-4xl mb-4">📋</div>
|
||||
<h3 className="text-xl font-semibold text-[--text-primary] mb-2">
|
||||
Review Your Information
|
||||
</h3>
|
||||
<p className="text-[--text-secondary]">
|
||||
Please review the information below and add any additional context
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[--background-tertiary] p-4 rounded-lg space-y-3">
|
||||
<div><strong>Company:</strong> {formData.companyName}</div>
|
||||
<div><strong>Industry:</strong> {formData.industry}</div>
|
||||
<div><strong>Size:</strong> {formData.size}</div>
|
||||
<div><strong>Mission:</strong> {formData.mission.substring(0, 100)}{formData.mission.length > 100 ? '...' : ''}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Additional Context
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.additionalContext}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, additionalContext: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Is there anything else important about your company that Auditly should know to provide better insights?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<div className="text-4xl mb-4">🎉</div>
|
||||
<h3 className="text-xl font-semibold text-[--text-primary]">
|
||||
Ready to Complete Setup!
|
||||
</h3>
|
||||
<p className="text-[--text-secondary]">
|
||||
{isGeneratingReport
|
||||
? 'Generating your personalized company insights...'
|
||||
: 'Once you complete this step, you\'ll have access to all Auditly features and your personalized company wiki will be generated.'
|
||||
}
|
||||
</p>
|
||||
{isGeneratingReport && (
|
||||
<div className="mt-4 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
<span className="ml-3 text-[--text-secondary]">Creating your company profile...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const canProceed = () => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return formData.companyName.trim().length > 0 && formData.industry && formData.size && formData.description.trim().length > 0;
|
||||
case 1:
|
||||
return formData.mission.trim().length > 0 && formData.vision.trim().length > 0;
|
||||
case 2:
|
||||
return formData.evolution.trim().length > 0;
|
||||
case 3:
|
||||
return formData.advantages.trim().length > 0 && formData.vulnerabilities.trim().length > 0;
|
||||
case 4:
|
||||
return formData.shortTermGoals.trim().length > 0 && formData.longTermGoals.trim().length > 0;
|
||||
case 5:
|
||||
return formData.cultureDescription.trim().length > 0 && formData.workEnvironment && formData.leadershipStyle.trim().length > 0 && formData.communicationStyle.trim().length > 0;
|
||||
case 6:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] flex items-center justify-center p-4">
|
||||
<div className="max-w-4xl w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
||||
Welcome to Auditly
|
||||
</h1>
|
||||
<p className="text-[--text-secondary]">
|
||||
Let's build a comprehensive profile of your organization to provide the best insights
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="mb-8">
|
||||
<FigmaProgress
|
||||
currentStep={step + 1}
|
||||
steps={steps.map((s, i) => ({ number: i + 1, title: s.title }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="max-w-none">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-2">
|
||||
{steps[step].title}
|
||||
</h2>
|
||||
<p className="text-[--text-secondary]">
|
||||
{steps[step].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{renderStep()}
|
||||
|
||||
<div className="flex justify-between mt-8">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={step === 0}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed() || isGeneratingReport}
|
||||
>
|
||||
{isGeneratingReport
|
||||
? 'Generating Wiki...'
|
||||
: step === steps.length - 1 ? 'Complete Setup & Generate Wiki' : 'Next'
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
218
pages/OrgSelection.tsx
Normal file
218
pages/OrgSelection.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useUserOrganizations } from '../contexts/UserOrganizationsContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
|
||||
const OrgSelection: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
organizations,
|
||||
loading,
|
||||
selectOrganization,
|
||||
createOrganization,
|
||||
joinOrganization
|
||||
} = useUserOrganizations();
|
||||
|
||||
const [showCreateOrg, setShowCreateOrg] = useState(false);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isJoining, setIsJoining] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for invite code in URL
|
||||
const hashSearch = location.hash.includes('?') ? location.hash.split('?')[1] : '';
|
||||
const searchParams = new URLSearchParams(hashSearch);
|
||||
const queryInvite = searchParams.get('invite');
|
||||
if (queryInvite) {
|
||||
setInviteCode(queryInvite);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
// Auto-join organization when invite code is present
|
||||
useEffect(() => {
|
||||
if (inviteCode && !isJoining && !loading) {
|
||||
handleJoinWithInvite();
|
||||
}
|
||||
}, [inviteCode, isJoining, loading]);
|
||||
|
||||
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') {
|
||||
navigate('/onboarding', { replace: true });
|
||||
} else {
|
||||
navigate('/reports', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateOrg = async () => {
|
||||
if (!newOrgName.trim() || isCreating) return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const result = await createOrganization(newOrgName);
|
||||
selectOrganization(result.orgId);
|
||||
|
||||
// Check if subscription setup is required
|
||||
if (result.requiresSubscription) {
|
||||
navigate(`/subscription-setup?orgId=${result.orgId}`, { replace: true });
|
||||
} else {
|
||||
navigate('/onboarding', { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create organization:', error);
|
||||
alert('Failed to create organization. Please try again.');
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinWithInvite = async () => {
|
||||
if (!inviteCode.trim() || isJoining) return;
|
||||
|
||||
setIsJoining(true);
|
||||
try {
|
||||
const orgId = await joinOrganization(inviteCode);
|
||||
selectOrganization(orgId);
|
||||
navigate('/employee-questionnaire', { replace: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to join organization:', error);
|
||||
alert('Failed to join organization. Please check your invite code.');
|
||||
} finally {
|
||||
setIsJoining(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8">Loading your organizations...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-12">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">Welcome to Auditly</h1>
|
||||
<p className="text-[--text-secondary]">Select an organization to continue</p>
|
||||
</div>
|
||||
|
||||
{/* Existing Organizations */}
|
||||
{organizations.length > 0 && (
|
||||
<Card className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-[--text-primary]">Your Organizations</h2>
|
||||
<div className="space-y-3">
|
||||
{organizations.map((org) => (
|
||||
<div
|
||||
key={org.orgId}
|
||||
className="flex items-center justify-between p-3 border border-[--border-color] rounded-lg hover:bg-[--background-secondary] cursor-pointer"
|
||||
onClick={() => handleSelectOrg(org.orgId)}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-[--text-primary]">{org.name}</div>
|
||||
<div className="text-sm text-[--text-secondary]">
|
||||
{org.role} • {org.onboardingCompleted ? 'Active' : 'Setup Required'}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">
|
||||
Enter
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Join with Invite */}
|
||||
{inviteCode && (
|
||||
<Card className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-[--text-primary]">Join Organization</h2>
|
||||
<p className="text-sm text-[--text-secondary] mb-4">
|
||||
You've been invited to join an organization with code: <code className="bg-[--background-tertiary] px-2 py-1 rounded text-[--text-primary]">{inviteCode}</code>
|
||||
</p>
|
||||
<Button onClick={handleJoinWithInvite} className="w-full" disabled={isJoining}>
|
||||
{isJoining ? 'Joining...' : 'Accept Invitation'}
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Create New Organization */}
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-4 text-[--text-primary]">Create New Organization</h2>
|
||||
{!showCreateOrg ? (
|
||||
<Button
|
||||
onClick={() => setShowCreateOrg(true)}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
+ Create New Organization
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Organization Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newOrgName}
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
placeholder="Enter organization name"
|
||||
className="w-full px-3 py-2 border border-[--input-border] rounded-lg bg-[--input-bg] text-[--text-primary] placeholder-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
onClick={handleCreateOrg}
|
||||
disabled={!newOrgName.trim() || isCreating}
|
||||
className="flex-1"
|
||||
>
|
||||
{isCreating ? 'Creating...' : 'Create Organization'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowCreateOrg(false)}
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Manual Invite Code Entry */}
|
||||
{!inviteCode && (
|
||||
<Card className="mt-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-[--text-primary]">Have an invite code?</h2>
|
||||
<div className="flex space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
value={inviteCode}
|
||||
onChange={(e) => setInviteCode(e.target.value)}
|
||||
placeholder="Enter invite code"
|
||||
className="flex-1 px-3 py-2 border border-[--input-border] rounded-lg bg-[--input-bg] text-[--text-primary] placeholder-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent]"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleJoinWithInvite}
|
||||
disabled={!inviteCode.trim() || isJoining}
|
||||
variant="secondary"
|
||||
>
|
||||
{isJoining ? 'Joining...' : 'Join'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrgSelection;
|
||||
218
pages/QuestionTypesDemo.tsx
Normal file
218
pages/QuestionTypesDemo.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card } from '../components/UiKit';
|
||||
import { QuestionInput } from '../components/ui/QuestionInput';
|
||||
import { Question } from '../components/ui/Question';
|
||||
import { FigmaQuestion } from '../components/figma/FigmaQuestion';
|
||||
import { EmployeeQuestion } from '../employeeQuestions';
|
||||
|
||||
const QuestionTypesDemo: React.FC = () => {
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||
|
||||
const demoQuestions: EmployeeQuestion[] = [
|
||||
{
|
||||
id: 'text_demo',
|
||||
prompt: 'What is your full name?',
|
||||
category: 'personal',
|
||||
type: 'text',
|
||||
placeholder: 'Enter your full name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'scale_demo',
|
||||
prompt: 'How satisfied are you with your role?',
|
||||
category: 'role',
|
||||
type: 'scale',
|
||||
scaleMin: 1,
|
||||
scaleMax: 10,
|
||||
scaleLabels: { min: 'Very dissatisfied', max: 'Very satisfied' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'yesno_demo',
|
||||
prompt: 'Do you have regular one-on-ones with your manager?',
|
||||
category: 'collaboration',
|
||||
type: 'yesno',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'textarea_demo',
|
||||
prompt: 'Describe your main responsibilities',
|
||||
category: 'role',
|
||||
type: 'textarea',
|
||||
placeholder: 'List your key daily tasks and responsibilities...',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'select_demo',
|
||||
prompt: 'What is your department?',
|
||||
category: 'role',
|
||||
type: 'select',
|
||||
options: ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance', 'Operations'],
|
||||
required: false,
|
||||
}
|
||||
];
|
||||
|
||||
const [currentDemoQuestion, setCurrentDemoQuestion] = useState(0);
|
||||
|
||||
const handleAnswerChange = (questionId: string, value: string) => {
|
||||
setAnswers(prev => ({ ...prev, [questionId]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
||||
Question Types Demo
|
||||
</h1>
|
||||
<p className="text-[--text-secondary]">
|
||||
Showcase of different question input types for the employee questionnaire
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Traditional Question Layout */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Traditional Layout
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
{demoQuestions.map((question, index) => (
|
||||
<Card key={question.id} className="p-6">
|
||||
<Question
|
||||
label={`${index + 1}. ${question.prompt}`}
|
||||
required={question.required}
|
||||
description={`Type: ${question.type} | Category: ${question.category}`}
|
||||
>
|
||||
<QuestionInput
|
||||
question={question}
|
||||
value={answers[question.id] || ''}
|
||||
onChange={(value) => handleAnswerChange(question.id, value)}
|
||||
/>
|
||||
</Question>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Figma Question Layout */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
||||
Figma Design Layout
|
||||
</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex gap-2 mb-4">
|
||||
{demoQuestions.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentDemoQuestion(index)}
|
||||
className={`px-3 py-1 rounded text-sm ${currentDemoQuestion === index
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Q{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<FigmaQuestion
|
||||
questionNumber={`Q${currentDemoQuestion + 1}`}
|
||||
title={demoQuestions[currentDemoQuestion].prompt}
|
||||
description={
|
||||
demoQuestions[currentDemoQuestion].required
|
||||
? 'Required'
|
||||
: 'Optional'
|
||||
}
|
||||
answer={answers[demoQuestions[currentDemoQuestion].id] || ''}
|
||||
onAnswerChange={(value) =>
|
||||
handleAnswerChange(demoQuestions[currentDemoQuestion].id, value)
|
||||
}
|
||||
onBack={
|
||||
currentDemoQuestion > 0
|
||||
? () => setCurrentDemoQuestion(prev => prev - 1)
|
||||
: undefined
|
||||
}
|
||||
onNext={
|
||||
currentDemoQuestion < demoQuestions.length - 1
|
||||
? () => setCurrentDemoQuestion(prev => prev + 1)
|
||||
: undefined
|
||||
}
|
||||
nextLabel={
|
||||
currentDemoQuestion < demoQuestions.length - 1
|
||||
? 'Next'
|
||||
: 'Finish'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Alternative Input Below Figma Question */}
|
||||
<div className="mt-6 max-w-2xl mx-auto">
|
||||
<Card className="p-6">
|
||||
<Question
|
||||
label={`Alternative Input for Q${currentDemoQuestion + 1}`}
|
||||
description={`Specialized ${demoQuestions[currentDemoQuestion].type} input`}
|
||||
>
|
||||
<QuestionInput
|
||||
question={demoQuestions[currentDemoQuestion]}
|
||||
value={answers[demoQuestions[currentDemoQuestion].id] || ''}
|
||||
onChange={(value) =>
|
||||
handleAnswerChange(demoQuestions[currentDemoQuestion].id, value)
|
||||
}
|
||||
/>
|
||||
</Question>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Answers Display */}
|
||||
<div className="mt-8">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">
|
||||
Current Answers
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{demoQuestions.map((question) => (
|
||||
<div key={question.id} className="grid grid-cols-1 md:grid-cols-3 gap-2 py-2 border-b border-[--border-color]">
|
||||
<div className="font-medium text-[--text-primary]">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="text-[--text-secondary] text-sm">
|
||||
{question.type}
|
||||
</div>
|
||||
<div className="text-[--text-primary]">
|
||||
{answers[question.id] || <span className="text-[--text-secondary] italic">No answer</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="mt-8 text-center">
|
||||
<div className="space-x-4">
|
||||
<a
|
||||
href="#/employee-questionnaire"
|
||||
className="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Traditional Questionnaire
|
||||
</a>
|
||||
<a
|
||||
href="#/employee-questionnaire-steps"
|
||||
className="inline-block px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
|
||||
>
|
||||
Stepped Questionnaire
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestionTypesDemo;
|
||||
69
pages/QuestionnaireComplete.tsx
Normal file
69
pages/QuestionnaireComplete.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { CheckCircle, FileText, Sparkles } from 'lucide-react';
|
||||
|
||||
const QuestionnaireComplete: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
employeeName = 'Employee',
|
||||
reportGenerated = false,
|
||||
message = 'Thank you for completing the questionnaire!'
|
||||
} = location.state || {};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-[--background-secondary] rounded-lg shadow-lg p-8 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
{reportGenerated ? (
|
||||
<div className="relative">
|
||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
||||
<Sparkles className="w-6 h-6 text-yellow-400 absolute -top-1 -right-1" />
|
||||
</div>
|
||||
) : (
|
||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
|
||||
Questionnaire Complete!
|
||||
</h1>
|
||||
|
||||
<p className="text-[--text-secondary] mb-6">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{reportGenerated && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<FileText className="w-5 h-5 text-blue-600 mr-2" />
|
||||
<span className="text-sm font-medium text-blue-600">AI Report Generated</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-300">
|
||||
Your personalized performance report has been created using AI analysis of your responses.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => navigate('/reports')}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
{reportGenerated ? 'View Your Report' : 'Go to Dashboard'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-full bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Return to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestionnaireComplete;
|
||||
210
pages/SubscriptionSetup.tsx
Normal file
210
pages/SubscriptionSetup.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useUserOrganizations } from '../contexts/UserOrganizationsContext';
|
||||
|
||||
const SubscriptionSetup: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
const { createCheckoutSession, getSubscriptionStatus } = useUserOrganizations();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orgId, setOrgId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const orgIdParam = searchParams.get('orgId');
|
||||
const sessionId = searchParams.get('session_id');
|
||||
const canceled = searchParams.get('canceled');
|
||||
|
||||
if (orgIdParam) {
|
||||
setOrgId(orgIdParam);
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
// Handle successful checkout
|
||||
handleSuccessfulCheckout(sessionId);
|
||||
} else if (canceled) {
|
||||
setError('Subscription setup was canceled. You can try again or use the 14-day trial.');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleSuccessfulCheckout = async (sessionId: string) => {
|
||||
if (!orgId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
// Get updated subscription status
|
||||
await getSubscriptionStatus(orgId);
|
||||
|
||||
// Redirect to onboarding to complete organization setup
|
||||
setTimeout(() => {
|
||||
navigate('/onboarding', { replace: true });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error handling successful checkout:', error);
|
||||
setError('There was an issue verifying your subscription. Please contact support.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartSubscription = async () => {
|
||||
if (!user || !orgId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const { sessionUrl } = await createCheckoutSession(orgId, user.email!);
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
window.location.href = sessionUrl;
|
||||
} catch (error) {
|
||||
console.error('Failed to create checkout session:', error);
|
||||
setError('Failed to start subscription setup. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipForNow = () => {
|
||||
// Allow user to continue with trial - go to onboarding to complete setup
|
||||
navigate('/onboarding', { replace: true });
|
||||
};
|
||||
|
||||
if (searchParams.get('session_id')) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Subscription Active!</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Your subscription has been successfully set up. Redirecting to complete your organization setup...
|
||||
</p>
|
||||
{loading && (
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Complete Your Setup</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Set up your subscription to unlock all features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-blue-800">14-Day Free Trial</h3>
|
||||
<p className="mt-2 text-sm text-blue-700">
|
||||
Start with a free trial. No payment required until the trial ends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Auditly Standard Plan</h3>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-600">Up to 50 employees</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-600">AI-powered employee reports</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-600">Company analytics & insights</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-600">AI chat assistant</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">$29<span className="text-sm font-normal text-gray-600">/month</span></div>
|
||||
<p className="text-xs text-gray-500">Billed monthly, cancel anytime</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleStartSubscription}
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
) : (
|
||||
'Start Subscription'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSkipForNow}
|
||||
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Continue with Trial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-center text-gray-500">
|
||||
Your trial will last 14 days. No payment required until the trial ends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionSetup;
|
||||
Reference in New Issue
Block a user