Fix up a ton of pages, settings/help/wiki need improvements, report generation needs slight updates. Otherwise completed
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -59,3 +59,10 @@ dist-ssr
|
|||||||
/figma-code
|
/figma-code
|
||||||
/*ignore.*
|
/*ignore.*
|
||||||
/document.svg
|
/document.svg
|
||||||
|
/AUTH_AGENT_MK2_SUMMARY.md
|
||||||
|
/ASSESSMENT_REPORT.md
|
||||||
|
/.tool-versions
|
||||||
|
/deploy-security.sh
|
||||||
|
/EMPLOYEE_FORMS_FIGMA_README.md
|
||||||
|
/TODOS.md
|
||||||
|
/SECURITY_MIGRATION.md
|
||||||
@@ -1 +0,0 @@
|
|||||||
elixir 1.18.4-otp-28
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
# Comprehensive Assessment Report: Figma vs Current Implementation
|
|
||||||
|
|
||||||
**Assessment Date:** August 24, 2025
|
|
||||||
**Assessor:** GitHub Copilot Assessing Agent Purple Elephant
|
|
||||||
**Total Figma Pages Analyzed:** 130 pages
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The Auditly application has achieved **excellent implementation coverage** of the core Figma designs. The major workflow areas (Login, Onboarding, Employee Forms, Reports, Chat) have been successfully implemented with proper Figma styling and functionality. The remaining gaps are primarily around UI polish, theme variants, and specific Company Wiki states.
|
|
||||||
|
|
||||||
**Overall Implementation Status: 85% Complete** ✅
|
|
||||||
|
|
||||||
## Detailed Analysis
|
|
||||||
|
|
||||||
### ✅ FULLY IMPLEMENTED (Major Areas)
|
|
||||||
|
|
||||||
#### 1. Login System (4 Figma Pages → Login.tsx)
|
|
||||||
- **Figma Pages:** `Login-Empty-State.jsx`, `Login-Filled-State.jsx`, `Login-Verification-Empty.jsx`, `Login-Verification-Filling.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/Login.tsx`
|
|
||||||
- **Status:** ✅ Complete - OTP verification is built into the main login component
|
|
||||||
- **Notes:** Includes email input, OTP verification, Google sign-in, and proper Figma styling
|
|
||||||
|
|
||||||
#### 2. Employee Forms System (40 Figma Pages → EmployeeQuestionnaireNew.tsx)
|
|
||||||
- **Figma Pages:** `Employee-Forms-Step-1.jsx` through `Employee-Forms-Step-38.jsx` (40 total)
|
|
||||||
- **Current Implementation:** `/src/pages/EmployeeQuestionnaireNew.tsx`
|
|
||||||
- **Status:** ✅ Complete per TODOS.md - Invite-based system implemented
|
|
||||||
- **Notes:** No-auth employee questionnaire with cloud function integration
|
|
||||||
|
|
||||||
#### 3. Onboarding System (62 Figma Pages → Onboarding.tsx)
|
|
||||||
- **Figma Pages:** `Onboarding-Step-1.jsx` through `Onboarding-Step-63.jsx` (note: Step 24 has typo "Onbaording")
|
|
||||||
- **Current Implementation:** `/src/pages/Onboarding.tsx`
|
|
||||||
- **Status:** ✅ Complete per TODOS.md - All 63 steps implemented
|
|
||||||
- **Notes:** Comprehensive step-by-step onboarding with proper API integration
|
|
||||||
|
|
||||||
#### 4. Reports System (2 Figma Pages → Reports.tsx)
|
|
||||||
- **Figma Pages:** `Company-Report.jsx`, `Employee-Report.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/Reports.tsx`
|
|
||||||
- **Status:** ✅ Complete per TODOS.md - Three-column layout with exact Figma styling
|
|
||||||
- **Notes:** Company report prioritized, employee reports alphabetical, PDF export
|
|
||||||
|
|
||||||
#### 5. Chat System (8 Figma Pages → Chat.tsx)
|
|
||||||
- **Figma Pages:** `Chat-AI-Response.jsx`, `Chat-File-Upload.jsx`, `Chat-Image-Upload.jsx`, `Chat-Image-And-File-Upload.jsx`, `Chat-Dark.jsx`, `Chat-Light.jsx`, `Chat-Mention-Employee-Menu.jsx`, `Chat-Mention-Employee-Text.jsx`, `CHat-Text-Area-Selected.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/Chat.tsx`
|
|
||||||
- **Status:** ✅ Complete - Uses Figma sidebar component
|
|
||||||
- **Notes:** Includes file upload, mentions, AI responses
|
|
||||||
|
|
||||||
### 🟡 PARTIALLY IMPLEMENTED (Needs Review)
|
|
||||||
|
|
||||||
#### 1. Company Wiki System (6 Figma Pages vs CompanyWiki.tsx)
|
|
||||||
- **Figma Pages:** `Company-Wiki-Empty-State-Dark.jsx`, `Company-Wiki-Empty-State-Light.jsx`, `Company-Wiki-Completed-State-Dark.jsx`, `Company-WIki-Completed-State-Light.jsx`, `Company-Wiki-Invite-Employees.jsx`, `Company-Wiki-Invite-Employee-Step-2-MultiSelect.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/CompanyWiki.tsx`
|
|
||||||
- **Status:** 🟡 Functional but needs Figma layout compliance review
|
|
||||||
- **Gap Analysis:**
|
|
||||||
- Missing exact three-column Figma layout
|
|
||||||
- Missing empty state designs
|
|
||||||
- Missing invite employee workflows
|
|
||||||
- Missing dark/light theme variants
|
|
||||||
|
|
||||||
#### 2. Settings System (1 Figma Page vs SettingsNew.tsx)
|
|
||||||
- **Figma Pages:** `Settings.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/SettingsNew.tsx`
|
|
||||||
- **Status:** 🟡 Implemented but needs Figma compliance verification
|
|
||||||
- **Gap Analysis:**
|
|
||||||
- Need to verify exact Figma layout matching
|
|
||||||
- Theme selection functionality present
|
|
||||||
|
|
||||||
#### 3. Help System (1 Figma Page vs HelpNew.tsx)
|
|
||||||
- **Figma Pages:** `Help.jsx`
|
|
||||||
- **Current Implementation:** `/src/pages/HelpNew.tsx`
|
|
||||||
- **Status:** 🟡 Implemented but needs Figma compliance verification
|
|
||||||
- **Gap Analysis:**
|
|
||||||
- Need to verify exact Figma layout matching
|
|
||||||
|
|
||||||
### ❌ IMPLEMENTATION GAPS
|
|
||||||
|
|
||||||
#### 1. Theme System (Dark/Light Variants)
|
|
||||||
- **Gap:** Many Figma pages have Dark/Light variants but theme switching is not fully implemented
|
|
||||||
- **Affected Pages:** All pages with `-Dark.jsx` and `-Light.jsx` variants
|
|
||||||
- **Recommendation:** Implement comprehensive dark theme with ThemeContext
|
|
||||||
|
|
||||||
#### 2. Submissions Pages Distinction
|
|
||||||
- **Figma Pages:** `Submissions-Dark.jsx`, `Submissions-Light.jsx`
|
|
||||||
- **Current Implementation:** `/submissions` route uses `EmployeeReport` in submissions mode
|
|
||||||
- **Status:** ❌ May need dedicated submissions page layout
|
|
||||||
- **Gap Analysis:** Need to determine if submissions should have distinct UI from reports
|
|
||||||
|
|
||||||
#### 3. OTP Verification as Standalone Page
|
|
||||||
- **Figma Pages:** `OTP-Verification.jsx`
|
|
||||||
- **Current Implementation:** Built into Login.tsx
|
|
||||||
- **Status:** ❌ Missing standalone OTP page
|
|
||||||
- **Note:** Functionality exists but not as separate route
|
|
||||||
|
|
||||||
## Deprecated/Unused Pages Analysis
|
|
||||||
|
|
||||||
### 🗑️ DEPRECATED PAGES (Can be removed)
|
|
||||||
|
|
||||||
The following pages in `/deprecated/pages/` are no longer needed:
|
|
||||||
|
|
||||||
1. **`deprecated/pages/Chat.tsx`** - Replaced by new `/src/pages/Chat.tsx`
|
|
||||||
2. **`deprecated/pages/EmployeeFormNew.tsx`** - Replaced by `/src/pages/EmployeeQuestionnaireNew.tsx`
|
|
||||||
3. **`deprecated/pages/OTPVerification.tsx`** - Functionality merged into `/src/pages/Login.tsx`
|
|
||||||
4. **`deprecated/pages/EmployeeFormsController.tsx`** - Replaced by new form system
|
|
||||||
5. **`deprecated/pages/OnboardingController.tsx`** - Replaced by `/src/pages/Onboarding.tsx`
|
|
||||||
6. **`deprecated/pages/EmployeeQuestionnaireMerged.tsx`** - Replaced by new questionnaire
|
|
||||||
7. **`deprecated/pages/DebugEmployee.tsx`** - Debug component, no longer needed
|
|
||||||
|
|
||||||
### 🤔 CURRENT PAGES NEEDING REVIEW
|
|
||||||
|
|
||||||
1. **`/src/pages/EmployeeQuestionnaire.tsx`** - Legacy version, kept for backwards compatibility
|
|
||||||
2. **`/src/pages/EmployeeQuestionnaireSteps.tsx`** - May be redundant
|
|
||||||
3. **`/src/pages/HelpAndSettings.tsx`** - Old combined version vs new separate pages
|
|
||||||
4. **`/src/pages/QuestionTypesDemo.tsx`** - Debug/demo page
|
|
||||||
5. **`/src/pages/FormsDashboard.tsx`** - Debug page
|
|
||||||
|
|
||||||
## Missing Figma Implementations
|
|
||||||
|
|
||||||
### High Priority Missing Features
|
|
||||||
|
|
||||||
1. **Company Wiki Complete Implementation**
|
|
||||||
- Empty state layouts (Dark/Light)
|
|
||||||
- Completed state layouts (Dark/Light)
|
|
||||||
- Employee invitation workflow
|
|
||||||
- Multi-select employee invitation
|
|
||||||
|
|
||||||
2. **Standalone OTP Verification Page**
|
|
||||||
- Dedicated route for OTP verification
|
|
||||||
- Email resend functionality
|
|
||||||
- Proper error states
|
|
||||||
|
|
||||||
3. **Dedicated Submissions Page**
|
|
||||||
- Verify if submissions needs distinct UI from reports
|
|
||||||
- Implement Dark/Light variants if needed
|
|
||||||
|
|
||||||
### Medium Priority Missing Features
|
|
||||||
|
|
||||||
1. **Dark Theme Implementation**
|
|
||||||
- Complete dark theme for all pages
|
|
||||||
- Theme toggle functionality
|
|
||||||
- Persistence across sessions
|
|
||||||
|
|
||||||
2. **Settings/Help Figma Compliance**
|
|
||||||
- Verify exact layout matching
|
|
||||||
- Implement missing UI elements
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Immediate Actions (High Priority)
|
|
||||||
|
|
||||||
1. **Review and enhance Company Wiki** to match exact Figma layouts
|
|
||||||
2. **Implement standalone OTP verification page** as separate route
|
|
||||||
3. **Clean up deprecated pages** from `/deprecated/` folder
|
|
||||||
4. **Verify submissions page requirements** - determine if distinct from reports
|
|
||||||
|
|
||||||
### Medium Term Actions
|
|
||||||
|
|
||||||
1. **Implement comprehensive dark theme** with proper toggle
|
|
||||||
2. **Review Settings and Help pages** for Figma compliance
|
|
||||||
3. **Remove or consolidate redundant pages** (legacy questionnaire versions)
|
|
||||||
|
|
||||||
### Long Term Actions
|
|
||||||
|
|
||||||
1. **Create component library documentation** for Figma components
|
|
||||||
2. **Implement automated Figma-to-code validation**
|
|
||||||
3. **Add theme variant testing** to ensure all pages work in both themes
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The Auditly application demonstrates **excellent implementation of core Figma designs**. The major user workflows are fully functional and styled according to Figma specifications. The remaining work is primarily around UI polish, theme variants, and specific edge cases.
|
|
||||||
|
|
||||||
**Key Achievements:**
|
|
||||||
- ✅ 4 major workflow areas fully implemented
|
|
||||||
- ✅ Proper Figma component library usage
|
|
||||||
- ✅ Invite-based employee system working
|
|
||||||
- ✅ 63-step onboarding completed
|
|
||||||
- ✅ Comprehensive reports system
|
|
||||||
|
|
||||||
**Next Steps:**
|
|
||||||
Focus on Company Wiki Figma compliance and dark theme implementation to achieve 95%+ coverage of Figma designs.
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
# Updated Employee Forms - Figma Design Implementation
|
|
||||||
|
|
||||||
This document describes the complete redesign and enhancement of the employee questionnaire system to match the exact Figma designs and implement the requirements from TODOS.md.
|
|
||||||
|
|
||||||
## 🎨 Design System Implementation
|
|
||||||
|
|
||||||
### Exact Figma Styling
|
|
||||||
- **Component Library**: Created precise React components matching Figma designs
|
|
||||||
- **Color System**: Updated CSS variables and Tailwind config with exact Figma colors
|
|
||||||
- **Typography**: Implemented Neue Montreal and Inter font families per Figma specs
|
|
||||||
- **Layout**: Pixel-perfect responsive layouts matching the 1440px design width
|
|
||||||
|
|
||||||
### Key Design Components
|
|
||||||
|
|
||||||
#### `/src/components/figma/FigmaEmployeeForms.tsx`
|
|
||||||
Complete component library including:
|
|
||||||
- `WelcomeScreen` - Landing page with Auditly branding
|
|
||||||
- `SectionIntro` - Section introduction pages with progress indicators
|
|
||||||
- `PersonalInfoForm` - Form inputs with exact Figma styling
|
|
||||||
- `TextAreaQuestion` - Multi-line text input components
|
|
||||||
- `RatingScaleQuestion` - Interactive 1-10 rating scales
|
|
||||||
- `YesNoChoice` - Binary choice components
|
|
||||||
- `ThankYouPage` - Completion confirmation
|
|
||||||
|
|
||||||
#### Color System Updates
|
|
||||||
```css
|
|
||||||
/* New Figma Design System Colors */
|
|
||||||
--Neutrals-NeutralSlate0 : #FFFFFF;
|
|
||||||
--Neutrals-NeutralSlate100 : #F5F5F5;
|
|
||||||
--Neutrals-NeutralSlate300 : #D5D7DA;
|
|
||||||
--Neutrals-NeutralSlate500 : #717680;
|
|
||||||
--Neutrals-NeutralSlate800 : #0A0D12;
|
|
||||||
--Neutrals-NeutralSlate950 : #0A0D12;
|
|
||||||
--Brand-Orange: #FF6B35;
|
|
||||||
--Other-White : #FFFFFF;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Enhanced Functionality
|
|
||||||
|
|
||||||
### Invite-Based System (No Authentication Required)
|
|
||||||
- **Direct Access**: Employees access forms via unique invite codes
|
|
||||||
- **Metadata Attachment**: Invite codes contain employee information
|
|
||||||
- **Pre-populated Data**: Forms auto-fill with invite metadata
|
|
||||||
- **Security**: One-time use invites with validation
|
|
||||||
|
|
||||||
### Company Owner Workflow
|
|
||||||
1. **Create Employee Invites**: Generate invites with employee metadata
|
|
||||||
2. **Send Invitations**: Share unique URLs with employees
|
|
||||||
3. **Track Progress**: Monitor form completion status
|
|
||||||
4. **View Reports**: Access AI-generated insights
|
|
||||||
|
|
||||||
### Employee Workflow
|
|
||||||
1. **Click Invite Link**: Access form directly (no account needed)
|
|
||||||
2. **Complete Assessment**: Step-by-step questionnaire
|
|
||||||
3. **Submit Responses**: Automatic processing via cloud functions
|
|
||||||
4. **Report Generation**: AI analysis with company context
|
|
||||||
|
|
||||||
## 🔗 Integration Points
|
|
||||||
|
|
||||||
### Cloud Functions Processing
|
|
||||||
```typescript
|
|
||||||
// Enhanced submission with company context
|
|
||||||
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
inviteCode: inviteCode,
|
|
||||||
answers: answers,
|
|
||||||
orgId: orgId,
|
|
||||||
includeCompanyContext: true // Include company Q&A for LLM
|
|
||||||
})
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### LLM Enhancement
|
|
||||||
- **Company Context**: Include company onboarding questions and answers
|
|
||||||
- **Alignment Analysis**: Compare employee responses to company values
|
|
||||||
- **Contextual Reports**: Generate reports with company-specific insights
|
|
||||||
- **Firestore Storage**: Save reports for dashboard display
|
|
||||||
|
|
||||||
## 📱 User Experience
|
|
||||||
|
|
||||||
### Progressive Disclosure
|
|
||||||
- **Welcome Screen**: Friendly introduction with branding
|
|
||||||
- **Section Intros**: Clear context for each question category
|
|
||||||
- **Progress Indicators**: Visual progress bars and step counters
|
|
||||||
- **Skip Options**: Allow users to skip non-critical questions
|
|
||||||
|
|
||||||
### Responsive Design
|
|
||||||
- **Mobile-First**: Optimized for all device sizes
|
|
||||||
- **Touch-Friendly**: Large buttons and touch targets
|
|
||||||
- **Accessibility**: Proper focus states and keyboard navigation
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- **Invite Validation**: Clear error messages for invalid/expired invites
|
|
||||||
- **Form Validation**: Real-time validation with helpful feedback
|
|
||||||
- **Network Issues**: Graceful handling of connectivity problems
|
|
||||||
- **Progress Saving**: Automatic form state preservation
|
|
||||||
|
|
||||||
## 🛠 Technical Implementation
|
|
||||||
|
|
||||||
### Route Structure
|
|
||||||
```typescript
|
|
||||||
// Invite-based (no auth)
|
|
||||||
/employee-form/:inviteCode
|
|
||||||
/questionnaire/:inviteCode
|
|
||||||
|
|
||||||
// Authenticated (legacy support)
|
|
||||||
/employee-questionnaire
|
|
||||||
/employee-questionnaire-legacy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Architecture
|
|
||||||
```
|
|
||||||
EmployeeQuestionnaireNew.tsx
|
|
||||||
├── WelcomeScreen
|
|
||||||
├── PersonalInfoForm
|
|
||||||
├── SectionIntro (6 sections)
|
|
||||||
├── Question Components
|
|
||||||
│ ├── TextAreaQuestion
|
|
||||||
│ ├── RatingScaleQuestion
|
|
||||||
│ └── YesNoChoice
|
|
||||||
└── ThankYouPage
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
- **Form Data**: Centralized state with TypeScript interfaces
|
|
||||||
- **Progress Tracking**: Step-by-step navigation
|
|
||||||
- **Error Handling**: Comprehensive error state management
|
|
||||||
- **Loading States**: User-friendly loading indicators
|
|
||||||
|
|
||||||
## 📊 Question Categories
|
|
||||||
|
|
||||||
### 1. Personal Information
|
|
||||||
- Name, email, company details
|
|
||||||
- Pre-populated from invite metadata
|
|
||||||
|
|
||||||
### 2. Role & Responsibilities
|
|
||||||
- Current title and department
|
|
||||||
- Daily responsibilities
|
|
||||||
- Role clarity assessment
|
|
||||||
|
|
||||||
### 3. Output & Accountability
|
|
||||||
- Weekly output rating
|
|
||||||
- Key deliverables
|
|
||||||
- KPIs and reporting structure
|
|
||||||
|
|
||||||
### 4. Team & Collaboration
|
|
||||||
- Close collaborators
|
|
||||||
- Team communication rating
|
|
||||||
- Support assessment
|
|
||||||
|
|
||||||
### 5. Tools & Resources
|
|
||||||
- Current tools and software
|
|
||||||
- Tool effectiveness rating
|
|
||||||
- Missing resources
|
|
||||||
|
|
||||||
### 6. Skills & Development
|
|
||||||
- Key skills and strengths
|
|
||||||
- Development goals
|
|
||||||
- Training awareness
|
|
||||||
- Career aspirations
|
|
||||||
|
|
||||||
### 7. Feedback & Improvement
|
|
||||||
- Company improvement suggestions
|
|
||||||
- Job satisfaction rating
|
|
||||||
- Additional feedback
|
|
||||||
|
|
||||||
## 🔐 Security & Privacy
|
|
||||||
|
|
||||||
### Invite Code Security
|
|
||||||
- **Unique Generation**: Cryptographically secure invite codes
|
|
||||||
- **One-Time Use**: Codes invalidated after submission
|
|
||||||
- **Expiration**: Time-based code expiration
|
|
||||||
- **Validation**: Server-side invite verification
|
|
||||||
|
|
||||||
### Data Protection
|
|
||||||
- **Encryption**: All data encrypted in transit and at rest
|
|
||||||
- **Access Control**: Role-based access to reports
|
|
||||||
- **Audit Trail**: Complete submission logging
|
|
||||||
- **Compliance**: GDPR and privacy regulation adherence
|
|
||||||
|
|
||||||
## 🚀 Deployment & Testing
|
|
||||||
|
|
||||||
### Development Testing
|
|
||||||
```bash
|
|
||||||
# Start development server
|
|
||||||
bun run dev
|
|
||||||
|
|
||||||
# Test invite flow
|
|
||||||
http://localhost:5173/#/employee-form/TEST_INVITE_CODE
|
|
||||||
|
|
||||||
# Test authenticated flow
|
|
||||||
http://localhost:5173/#/employee-questionnaire
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
- **Environment Variables**: Secure API endpoint configuration
|
|
||||||
- **CDN Integration**: Optimized asset delivery
|
|
||||||
- **Error Monitoring**: Comprehensive error tracking
|
|
||||||
- **Performance Monitoring**: Real-time performance metrics
|
|
||||||
|
|
||||||
## 📈 Analytics & Reporting
|
|
||||||
|
|
||||||
### Completion Metrics
|
|
||||||
- **Response Rates**: Track invitation to completion rates
|
|
||||||
- **Drop-off Analysis**: Identify form abandonment points
|
|
||||||
- **Time Analysis**: Monitor completion times
|
|
||||||
- **Quality Scores**: Assess response completeness
|
|
||||||
|
|
||||||
### Report Generation
|
|
||||||
- **AI Processing**: Cloud function LLM analysis
|
|
||||||
- **Company Alignment**: Compare with organizational values
|
|
||||||
- **Actionable Insights**: Specific improvement recommendations
|
|
||||||
- **Dashboard Integration**: Seamless report display
|
|
||||||
|
|
||||||
## 🔄 Migration Strategy
|
|
||||||
|
|
||||||
### Backwards Compatibility
|
|
||||||
- **Legacy Routes**: Maintain existing functionality
|
|
||||||
- **Gradual Migration**: Phased rollout approach
|
|
||||||
- **Data Consistency**: Ensure data format compatibility
|
|
||||||
- **Feature Parity**: All existing features preserved
|
|
||||||
|
|
||||||
### Rollout Plan
|
|
||||||
1. **Phase 1**: Deploy new components alongside existing
|
|
||||||
2. **Phase 2**: Update invite generation to use new routes
|
|
||||||
3. **Phase 3**: Migrate existing authenticated users
|
|
||||||
4. **Phase 4**: Deprecate legacy routes
|
|
||||||
|
|
||||||
## 🛡 Error Handling & Recovery
|
|
||||||
|
|
||||||
### Common Error Scenarios
|
|
||||||
- **Invalid Invite**: Clear messaging with support contact
|
|
||||||
- **Network Issues**: Retry mechanisms with user feedback
|
|
||||||
- **Form Validation**: Real-time validation with helpful hints
|
|
||||||
- **Submission Failures**: Automatic retry with progress preservation
|
|
||||||
|
|
||||||
### Recovery Mechanisms
|
|
||||||
- **Auto-save**: Periodic form state saving
|
|
||||||
- **Session Recovery**: Resume from last saved state
|
|
||||||
- **Support Integration**: Direct access to help resources
|
|
||||||
- **Manual Backup**: Export form data for recovery
|
|
||||||
|
|
||||||
This comprehensive redesign ensures the employee forms system meets all requirements while providing an exceptional user experience that matches the Figma designs exactly.
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# Security Migration: Frontend to Cloud Functions
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This migration addresses critical security vulnerabilities by moving all Firestore interactions from the frontend to secure cloud functions. This prevents unauthorized data access and protects your database structure from being exposed to users.
|
|
||||||
|
|
||||||
## Security Issues Addressed
|
|
||||||
|
|
||||||
### Before (Vulnerable)
|
|
||||||
- ❌ Direct Firestore access from frontend
|
|
||||||
- ❌ Database schema exposed to users
|
|
||||||
- ❌ API keys visible in browser
|
|
||||||
- ❌ Firestore rules can be analyzed by attackers
|
|
||||||
- ❌ Users can potentially bypass frontend logic
|
|
||||||
|
|
||||||
### After (Secure)
|
|
||||||
- ✅ All data operations go through authenticated cloud functions
|
|
||||||
- ✅ Database structure hidden from frontend
|
|
||||||
- ✅ User authorization verified on every request
|
|
||||||
- ✅ No sensitive data exposed to client
|
|
||||||
- ✅ Complete audit trail of data access
|
|
||||||
|
|
||||||
## Migration Changes
|
|
||||||
|
|
||||||
### 1. Cloud Functions (Backend)
|
|
||||||
**File: `functions/index.js`**
|
|
||||||
|
|
||||||
Added secure endpoints:
|
|
||||||
- `getOrgData` - Get organization data with auth
|
|
||||||
- `updateOrgData` - Update organization data with auth
|
|
||||||
- `getEmployees` - Get employees with auth
|
|
||||||
- `getSubmissions` - Get submissions with auth
|
|
||||||
- `getReports` - Get reports with auth
|
|
||||||
- `upsertEmployee` - Create/update employees with auth
|
|
||||||
- `saveReport` - Save reports with auth
|
|
||||||
- `getCompanyReports` - Get company reports with auth
|
|
||||||
|
|
||||||
Each endpoint:
|
|
||||||
- Verifies user authentication
|
|
||||||
- Checks user authorization for the organization
|
|
||||||
- Validates all inputs
|
|
||||||
- Returns appropriate error messages
|
|
||||||
|
|
||||||
### 2. Secure API Service (Frontend)
|
|
||||||
**File: `src/services/secureApi.ts`**
|
|
||||||
|
|
||||||
New service that:
|
|
||||||
- Handles all communication with cloud functions
|
|
||||||
- Provides type-safe methods for data operations
|
|
||||||
- Manages authentication tokens
|
|
||||||
- Handles errors gracefully
|
|
||||||
|
|
||||||
### 3. Updated Context (Frontend)
|
|
||||||
**File: `src/contexts/OrgContext.tsx`**
|
|
||||||
|
|
||||||
Completely rewritten to:
|
|
||||||
- Use secure API instead of direct Firestore
|
|
||||||
- Load data on component mount instead of real-time listeners
|
|
||||||
- Provide loading states for better UX
|
|
||||||
- Handle authentication properly
|
|
||||||
|
|
||||||
### 4. Firestore Rules (Security)
|
|
||||||
**File: `firestore.rules`**
|
|
||||||
|
|
||||||
Updated rules to:
|
|
||||||
```javascript
|
|
||||||
// DENY ALL direct client access
|
|
||||||
allow read, write: if false;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Steps
|
|
||||||
|
|
||||||
1. **Deploy Cloud Functions**
|
|
||||||
```bash
|
|
||||||
cd functions
|
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
firebase deploy --only functions
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Deploy Firestore Rules**
|
|
||||||
```bash
|
|
||||||
firebase deploy --only firestore:rules
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Use Deployment Script**
|
|
||||||
```bash
|
|
||||||
./deploy-security.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend Usage
|
|
||||||
|
|
||||||
### Before (Direct Firestore)
|
|
||||||
```typescript
|
|
||||||
// DON'T DO THIS ANYMORE
|
|
||||||
import { collection, doc, getDoc } from 'firebase/firestore';
|
|
||||||
const orgDoc = await getDoc(doc(db, 'orgs', orgId));
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (Secure API)
|
|
||||||
```typescript
|
|
||||||
// DO THIS INSTEAD
|
|
||||||
import { secureApi } from '../services/secureApi';
|
|
||||||
const orgData = await secureApi.getOrgData(orgId, userId);
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Methods Available
|
|
||||||
|
|
||||||
### Organization Data
|
|
||||||
- `secureApi.getOrgData(orgId, userId)`
|
|
||||||
- `secureApi.updateOrgData(orgId, userId, data)`
|
|
||||||
|
|
||||||
### Employees
|
|
||||||
- `secureApi.getEmployees(orgId, userId)`
|
|
||||||
- `secureApi.upsertEmployee(orgId, userId, employeeData)`
|
|
||||||
|
|
||||||
### Submissions & Reports
|
|
||||||
- `secureApi.getSubmissions(orgId, userId)`
|
|
||||||
- `secureApi.getReports(orgId, userId)`
|
|
||||||
- `secureApi.saveReport(orgId, userId, employeeId, reportData)`
|
|
||||||
|
|
||||||
### Company Reports
|
|
||||||
- `secureApi.getCompanyReports(orgId, userId)`
|
|
||||||
- `secureApi.saveCompanyReport(orgId, report)`
|
|
||||||
|
|
||||||
### Existing API (Already Secure)
|
|
||||||
- `secureApi.sendOTP(email, inviteCode?)`
|
|
||||||
- `secureApi.verifyOTP(email, otp)`
|
|
||||||
- `secureApi.createInvitation(...)`
|
|
||||||
- `secureApi.generateEmployeeReport(...)`
|
|
||||||
- `secureApi.generateCompanyWiki(...)`
|
|
||||||
- `secureApi.chat(...)`
|
|
||||||
|
|
||||||
## Authentication Flow
|
|
||||||
|
|
||||||
1. User logs in via OTP (cloud function)
|
|
||||||
2. Cloud function returns user data and token
|
|
||||||
3. Frontend stores user data in AuthContext
|
|
||||||
4. All API calls include user ID for authorization
|
|
||||||
5. Cloud functions verify user access to organization data
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The secure API provides consistent error handling:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const data = await secureApi.getOrgData(orgId, userId);
|
|
||||||
// Handle success
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error - could be auth, network, or data error
|
|
||||||
console.error('Failed to load organization:', error.message);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- **Loading States**: UI shows loading while data fetches
|
|
||||||
- **Caching**: Local state caching reduces API calls
|
|
||||||
- **Batch Operations**: Multiple related operations in single calls
|
|
||||||
- **Error Recovery**: Graceful fallbacks for network issues
|
|
||||||
|
|
||||||
## Security Benefits
|
|
||||||
|
|
||||||
1. **Zero Trust**: Every request is authenticated and authorized
|
|
||||||
2. **Data Hiding**: Database schema not exposed to frontend
|
|
||||||
3. **Audit Trail**: All access logged in cloud functions
|
|
||||||
4. **Input Validation**: All data validated server-side
|
|
||||||
5. **Rate Limiting**: Can be added to cloud functions
|
|
||||||
6. **IP Filtering**: Can be implemented at cloud function level
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
After migration, verify:
|
|
||||||
|
|
||||||
1. ✅ Users can only access their organization's data
|
|
||||||
2. ✅ Unauthenticated requests are rejected
|
|
||||||
3. ✅ Invalid organization IDs are rejected
|
|
||||||
4. ✅ All CRUD operations work through secure API
|
|
||||||
5. ✅ Error messages don't leak sensitive information
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Monitor these metrics:
|
|
||||||
- API response times
|
|
||||||
- Authentication failures
|
|
||||||
- Authorization failures
|
|
||||||
- Data access patterns
|
|
||||||
- Error rates
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
This secure foundation enables:
|
|
||||||
- Role-based access control (RBAC)
|
|
||||||
- Data encryption at rest
|
|
||||||
- Advanced audit logging
|
|
||||||
- API rate limiting
|
|
||||||
- IP whitelisting
|
|
||||||
- Multi-factor authentication
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
1. Check browser console for detailed error messages
|
|
||||||
2. Verify Firebase project configuration
|
|
||||||
3. Ensure cloud functions are deployed successfully
|
|
||||||
4. Check Firestore rules are updated
|
|
||||||
|
|
||||||
The secure API provides comprehensive error messages to help debug issues during development.
|
|
||||||
73
TODOS.md
73
TODOS.md
@@ -1,73 +0,0 @@
|
|||||||
# Agents
|
|
||||||
|
|
||||||
- The following is true for all agents:
|
|
||||||
- The style from the figma should be preserved, the figma style takes highest priority on style, our opinions and preferences are not relevant. However, functionality is just as important, and if functionality is already implemnted, preseve it, as well as if the task says; update the functionality.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Login Agent
|
|
||||||
|
|
||||||
- Copy the login steps style and scaffolding available in /figma-code/pages/Login-*.jsx into our current login process
|
|
||||||
|
|
||||||
## Employee Forms Agent ✅ COMPLETED
|
|
||||||
|
|
||||||
- ✅ Copy the employee forms style and scaffolding available in /figma-code/pages/Employee-Forms-*.jsx into our current employee forms process.
|
|
||||||
- ✅ The current employee forms process may not be correct, we need to have it so that the employees do not require any account to fill out the forms.
|
|
||||||
- ✅ The company owner should just be able to invite their employees, and each employee gets their own invite.
|
|
||||||
- ✅ The invite will have the employee metadata attached to it, and this invite is all that is needed for verification.
|
|
||||||
- ✅ Upon an employee submitting their form, we must run it through our LLM via sending the form to cloud functions.
|
|
||||||
- ✅ When we are generating the employee report, we also need to supply the LLM with the company's onboarding questions and answers, so that the LLM has knowledge of what the company aligns with, and how the employee aligns with the company.
|
|
||||||
- ✅ We need to store this report in firestore, so that it may be displayed on our reports page.
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- Created exact Figma component library in `/src/components/figma/FigmaEmployeeForms.tsx`
|
|
||||||
- Implemented invite-based employee questionnaire in `/src/pages/EmployeeQuestionnaireNew.tsx`
|
|
||||||
- Updated color system in `index.css` and `tailwind.config.js` with exact Figma tokens
|
|
||||||
- Configured routing for invite-based access: `/employee-form/:inviteCode`
|
|
||||||
- Maintained cloud function integration for LLM processing
|
|
||||||
- Included company context in submission for alignment analysis
|
|
||||||
- See `EMPLOYEE_FORMS_FIGMA_README.md` for complete documentation
|
|
||||||
|
|
||||||
## Reports Agent ✅ COMPLETED
|
|
||||||
|
|
||||||
- ✅ The company report is always the report found at the top-most of the reports, and following are all employees alphabetically.
|
|
||||||
- ✅ You must copy the style AND information sections from /figma-code/pages/Company-Report.jsx and /figma-code/pages/Employee-Report.jsx into the application.
|
|
||||||
- ✅ You must dynamically fill out all of the correlating sections with truthy information from the Firestore documents.
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- Created comprehensive Reports.tsx component with exact three-column Figma layout
|
|
||||||
- Left sidebar: Complete navigation with company logo, nav items, settings, and CTA card
|
|
||||||
- Middle sidebar: Employee list with search functionality and alphabetical sorting
|
|
||||||
- Main content: Dynamic report display with company report prioritized at top
|
|
||||||
- Implemented all major company report sections: Weaknesses, Personnel Changes, Hiring Needs, Forward Plan, Strengths, and Grading Overview
|
|
||||||
- Updated App.tsx routing to use new Reports component instead of legacy EmployeeReport
|
|
||||||
- Added comprehensive CSS color system with all Figma design tokens
|
|
||||||
- Maintained existing functionality: data loading, report generation, PDF export
|
|
||||||
- Component shows company report by default for owners, with employee reports listed alphabetically
|
|
||||||
- See `/src/pages/Reports.tsx` for complete implementation
|
|
||||||
|
|
||||||
## Onboarding Agent ✅ COMPLETED
|
|
||||||
|
|
||||||
- ✅ Currently, the design for the frames is pretty much there, however there 63 steps for the onboarding, currently only 8 of them are implmented.
|
|
||||||
- ✅ Be sure to implement every step listed in /figma-code/Onboarding-Step-*.jsx
|
|
||||||
- ✅ Then, when submitting the onboarding questions, we should make a request to submitting this data via `/home/ra/auditly/src/services/secureApi.ts`
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- Created comprehensive 63-step onboarding system in `/src/data/onboardingSteps.ts`
|
|
||||||
- Built exact Figma component library in `/src/components/onboarding/FigmaOnboardingComponents.tsx`
|
|
||||||
- Updated main Onboarding.tsx component to use new 63-step structure with proper Figma styling
|
|
||||||
- Organized steps into 7 logical sections: Company Overview & Mission, Leadership & Organizational Structure, Operations & Execution, Culture & Team Health, Sales Marketing & Growth, Innovation & Product/Service Strategy, Personal Leadership & Risk
|
|
||||||
- Implemented all step types: intro (section introductions), question (open-ended text), multiple_choice (option selection), form (company details)
|
|
||||||
- Added proper API integration using `secureApi.ts` with `onboarding/complete` endpoint
|
|
||||||
- Maintained exact Figma styling with progress indicators, section navigation, and responsive layouts
|
|
||||||
- Enhanced CSS with all necessary Figma color tokens and variables
|
|
||||||
|
|
||||||
## White Screen Agent
|
|
||||||
|
|
||||||
- Currently, when the app is started, it immediately goes to a all white page, we never make it anywhere else. We need to fix this.
|
|
||||||
|
|
||||||
## Assessing Agent
|
|
||||||
|
|
||||||
- You are to go through all of the figmas, and then go through all of the current pages.
|
|
||||||
- You will note down which pages are deprecated / unused due to incomplete implementation or redundancy.
|
|
||||||
- You will create a detailed report on which pages need to be implemented still, and what we currently do not have that the figma has.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Deploy Firestore security rules
|
|
||||||
echo "🔒 Deploying secure Firestore rules..."
|
|
||||||
firebase deploy --only firestore:rules
|
|
||||||
|
|
||||||
# Deploy cloud functions with new secure endpoints
|
|
||||||
echo "☁️ Deploying cloud functions..."
|
|
||||||
firebase deploy --only functions
|
|
||||||
|
|
||||||
echo "✅ Security migration complete!"
|
|
||||||
echo ""
|
|
||||||
echo "🔒 Security improvements implemented:"
|
|
||||||
echo " - All direct Firestore client access is now blocked"
|
|
||||||
echo " - Data operations go through authenticated cloud functions"
|
|
||||||
echo " - User authorization is verified on every request"
|
|
||||||
echo " - Database structure is hidden from clients"
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ Important: Make sure to update your frontend to use the secure API"
|
|
||||||
echo " - Replace all direct Firestore calls with secureApi methods"
|
|
||||||
echo " - Update components to use the new OrgContext implementation"
|
|
||||||
echo ""
|
|
||||||
@@ -10,6 +10,66 @@ admin.initializeApp({
|
|||||||
});
|
});
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
|
|
||||||
|
// Auth middleware function to validate tokens and extract user context
|
||||||
|
const validateAuthAndGetContext = async (req) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
throw new Error('Missing or invalid authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
|
// Validate token format (should start with 'session_')
|
||||||
|
if (!token.startsWith('session_')) {
|
||||||
|
throw new Error('Invalid token format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up token in Firestore
|
||||||
|
const tokenDoc = await db.collection("authTokens").doc(token).get();
|
||||||
|
|
||||||
|
if (!tokenDoc.exists) {
|
||||||
|
throw new Error('Token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = tokenDoc.data();
|
||||||
|
|
||||||
|
if (!tokenData.isActive) {
|
||||||
|
throw new Error('Token is inactive');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() > tokenData.expiresAt) {
|
||||||
|
throw new Error('Token has expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last used timestamp
|
||||||
|
await tokenDoc.ref.update({ lastUsedAt: Date.now() });
|
||||||
|
|
||||||
|
// Get user's organizations
|
||||||
|
const userOrgsSnapshot = await db.collection("users").doc(tokenData.userId).collection("organizations").get();
|
||||||
|
const orgIds = userOrgsSnapshot.docs.map(doc => doc.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: tokenData.userId,
|
||||||
|
orgIds: orgIds,
|
||||||
|
// For backward compatibility, use first org as default
|
||||||
|
orgId: orgIds[0] || null,
|
||||||
|
token: token
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to verify user has access to specific organization
|
||||||
|
const verifyOrgAccess = (authContext, targetOrgId) => {
|
||||||
|
if (!targetOrgId) {
|
||||||
|
return authContext.orgId; // Use default org
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authContext.orgIds.includes(targetOrgId)) {
|
||||||
|
throw new Error('Unauthorized access to organization');
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetOrgId;
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize OpenAI if API key is available
|
// Initialize OpenAI if API key is available
|
||||||
const openai = process.env.OPENAI_API_KEY ? new OpenAI({
|
const openai = process.env.OPENAI_API_KEY ? new OpenAI({
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
@@ -287,6 +347,15 @@ exports.verifyOTP = onRequest({ cors: true }, async (req, res) => {
|
|||||||
// Generate a simple session token (in production, use proper JWT)
|
// Generate a simple session token (in production, use proper JWT)
|
||||||
const customToken = `session_${userId}_${Date.now()}`;
|
const customToken = `session_${userId}_${Date.now()}`;
|
||||||
|
|
||||||
|
// Store auth token in Firestore for validation
|
||||||
|
await db.collection("authTokens").doc(customToken).set({
|
||||||
|
userId: userId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 days
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
// Handle invitation if present
|
// Handle invitation if present
|
||||||
let inviteData = null;
|
let inviteData = null;
|
||||||
if (otpData.inviteCode) {
|
if (otpData.inviteCode) {
|
||||||
@@ -330,13 +399,20 @@ exports.createInvitation = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, name, email, role = "employee", department } = req.body;
|
|
||||||
|
|
||||||
if (!orgId || !email || !name) {
|
|
||||||
return res.status(400).json({ error: "Organization ID, name, and email are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
const { name, email, role = "employee", department } = req.body;
|
||||||
|
|
||||||
|
if (!email || !name) {
|
||||||
|
return res.status(400).json({ error: "Name and email are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the user's default organization (first one)
|
||||||
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
|
}
|
||||||
// Generate invite code
|
// Generate invite code
|
||||||
const code = Math.random().toString(36).substring(2, 15);
|
const code = Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
@@ -531,25 +607,18 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, employeeId, answers, inviteCode } = req.body;
|
const { employeeId, answers, inviteCode } = req.body;
|
||||||
|
|
||||||
// For invite-based submissions, we need inviteCode and answers
|
|
||||||
// For regular submissions, we need orgId, employeeId, and answers
|
|
||||||
if (inviteCode) {
|
|
||||||
if (!inviteCode || !answers) {
|
|
||||||
return res.status(400).json({ error: "Invite code and answers are required for invite submissions" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!orgId || !employeeId || !answers) {
|
|
||||||
return res.status(400).json({ error: "Organization ID, employee ID, and answers are required" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let finalOrgId, finalEmployeeId;
|
let finalOrgId, finalEmployeeId;
|
||||||
|
|
||||||
if (inviteCode) {
|
if (inviteCode) {
|
||||||
// For invite-based submissions, look up the invite to get employee and org data
|
// Invite-based submission (no auth required)
|
||||||
|
if (!inviteCode || !answers) {
|
||||||
|
return res.status(400).json({ error: "Invite code and answers are required for invite submissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the invite to get employee and org data
|
||||||
const inviteSnapshot = await db
|
const inviteSnapshot = await db
|
||||||
.collectionGroup("invites")
|
.collectionGroup("invites")
|
||||||
.where("code", "==", inviteCode)
|
.where("code", "==", inviteCode)
|
||||||
@@ -565,9 +634,19 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
|||||||
finalOrgId = invite.orgId;
|
finalOrgId = invite.orgId;
|
||||||
finalEmployeeId = invite.employee.id;
|
finalEmployeeId = invite.employee.id;
|
||||||
} else {
|
} else {
|
||||||
// Regular submission
|
// Authenticated submission
|
||||||
finalOrgId = orgId;
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
|
||||||
|
if (!employeeId || !answers) {
|
||||||
|
return res.status(400).json({ error: "Employee ID and answers are required for authenticated submissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
finalOrgId = authContext.orgId;
|
||||||
finalEmployeeId = employeeId;
|
finalEmployeeId = employeeId;
|
||||||
|
|
||||||
|
if (!finalOrgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store submission
|
// Store submission
|
||||||
@@ -1002,13 +1081,14 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, userId } = req.body;
|
|
||||||
|
|
||||||
if (!name || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization name and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: "Organization name is required" });
|
||||||
|
}
|
||||||
// Generate unique organization ID
|
// Generate unique organization ID
|
||||||
const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
@@ -1018,7 +1098,7 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
onboardingCompleted: false,
|
onboardingCompleted: false,
|
||||||
ownerId: userId,
|
ownerId: authContext.userId,
|
||||||
// Subscription fields (will be populated after Stripe setup)
|
// Subscription fields (will be populated after Stripe setup)
|
||||||
subscription: {
|
subscription: {
|
||||||
status: 'trial', // trial, active, past_due, canceled
|
status: 'trial', // trial, active, past_due, canceled
|
||||||
@@ -1049,20 +1129,20 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
await orgRef.set(orgData);
|
await orgRef.set(orgData);
|
||||||
|
|
||||||
// Get user information from Firestore (since we don't use Firebase Auth)
|
// Get user information from Firestore (since we don't use Firebase Auth)
|
||||||
const userRef = db.collection("users").doc(userId);
|
const userRef = db.collection("users").doc(authContext.userId);
|
||||||
const userDoc = await userRef.get();
|
const userDoc = await userRef.get();
|
||||||
|
|
||||||
if (!userDoc.exists) {
|
if (!userDoc.exists) {
|
||||||
console.error("User document not found:", userId);
|
console.error("User document not found:", authContext.userId);
|
||||||
return res.status(400).json({ error: "User not found" });
|
return res.status(400).json({ error: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = userDoc.data();
|
const userData = userDoc.data();
|
||||||
|
|
||||||
// Add user as owner to organization's employees collection
|
// Add user as owner to organization's employees collection
|
||||||
const employeeRef = orgRef.collection("employees").doc(userId);
|
const employeeRef = orgRef.collection("employees").doc(authContext.userId);
|
||||||
await employeeRef.set({
|
await employeeRef.set({
|
||||||
id: userId,
|
id: authContext.userId,
|
||||||
role: "owner",
|
role: "owner",
|
||||||
isOwner: true,
|
isOwner: true,
|
||||||
joinedAt: Date.now(),
|
joinedAt: Date.now(),
|
||||||
@@ -1073,7 +1153,7 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add organization to user's organizations (for multi-org support)
|
// Add organization to user's organizations (for multi-org support)
|
||||||
const userOrgRef = db.collection("users").doc(userId).collection("organizations").doc(orgId);
|
const userOrgRef = db.collection("users").doc(authContext.userId).collection("organizations").doc(orgId);
|
||||||
await userOrgRef.set({
|
await userOrgRef.set({
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
@@ -1115,17 +1195,14 @@ exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = req.query.userId || req.params.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: "User ID is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
|
||||||
// Get user's organizations
|
// Get user's organizations
|
||||||
const userOrgsSnapshot = await db
|
const userOrgsSnapshot = await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.doc(userId)
|
.doc(authContext.userId)
|
||||||
.collection("organizations")
|
.collection("organizations")
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
@@ -1143,6 +1220,10 @@ exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get user organizations error:", error);
|
console.error("Get user organizations error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to get user organizations" });
|
res.status(500).json({ error: "Failed to get user organizations" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1158,13 +1239,14 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId, inviteCode } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !inviteCode) {
|
|
||||||
return res.status(400).json({ error: "User ID and invite code are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
const { inviteCode } = req.body;
|
||||||
|
|
||||||
|
if (!inviteCode) {
|
||||||
|
return res.status(400).json({ error: "Invite code is required" });
|
||||||
|
}
|
||||||
// Find the invitation
|
// Find the invitation
|
||||||
const inviteSnapshot = await db
|
const inviteSnapshot = await db
|
||||||
.collectionGroup("invites")
|
.collectionGroup("invites")
|
||||||
@@ -1196,11 +1278,11 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
const orgData = orgDoc.data();
|
const orgData = orgDoc.data();
|
||||||
|
|
||||||
// Get user information from Firestore (since we don't use Firebase Auth)
|
// Get user information from Firestore (since we don't use Firebase Auth)
|
||||||
const userRef = db.collection("users").doc(userId);
|
const userRef = db.collection("users").doc(authContext.userId);
|
||||||
const userDoc = await userRef.get();
|
const userDoc = await userRef.get();
|
||||||
|
|
||||||
if (!userDoc.exists) {
|
if (!userDoc.exists) {
|
||||||
console.error("User document not found:", userId);
|
console.error("User document not found:", authContext.userId);
|
||||||
return res.status(400).json({ error: "User not found" });
|
return res.status(400).json({ error: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,7 +1291,7 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
// Mark invitation as consumed
|
// Mark invitation as consumed
|
||||||
await inviteDoc.ref.update({
|
await inviteDoc.ref.update({
|
||||||
status: "consumed",
|
status: "consumed",
|
||||||
consumedBy: userId,
|
consumedBy: authContext.userId,
|
||||||
consumedAt: Date.now(),
|
consumedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1218,9 +1300,9 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
.collection("orgs")
|
.collection("orgs")
|
||||||
.doc(orgId)
|
.doc(orgId)
|
||||||
.collection("employees")
|
.collection("employees")
|
||||||
.doc(userId)
|
.doc(authContext.userId)
|
||||||
.set({
|
.set({
|
||||||
id: userId,
|
id: authContext.userId,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
name: userData.displayName || userData.email.split("@")[0],
|
name: userData.displayName || userData.email.split("@")[0],
|
||||||
role: invite.role || "employee",
|
role: invite.role || "employee",
|
||||||
@@ -1231,7 +1313,7 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
// Add organization to user's organizations
|
// Add organization to user's organizations
|
||||||
await db
|
await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.doc(userId)
|
.doc(authContext.userId)
|
||||||
.collection("organizations")
|
.collection("organizations")
|
||||||
.doc(orgId)
|
.doc(orgId)
|
||||||
.set({
|
.set({
|
||||||
@@ -1272,17 +1354,24 @@ exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId, userEmail, priceId } = req.body;
|
|
||||||
|
|
||||||
if (!orgId || !userId || !userEmail) {
|
|
||||||
return res.status(400).json({ error: "Organization ID, user ID, and email are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stripe) {
|
|
||||||
return res.status(500).json({ error: "Stripe not configured" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
const { userEmail, priceId } = req.body;
|
||||||
|
|
||||||
|
if (!userEmail) {
|
||||||
|
return res.status(400).json({ error: "User email is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stripe) {
|
||||||
|
return res.status(500).json({ error: "Stripe not configured" });
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create Stripe customer
|
// Get or create Stripe customer
|
||||||
let customer;
|
let customer;
|
||||||
const existingCustomers = await stripe.customers.list({
|
const existingCustomers = await stripe.customers.list({
|
||||||
@@ -1296,7 +1385,7 @@ exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
|
|||||||
customer = await stripe.customers.create({
|
customer = await stripe.customers.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
metadata: {
|
metadata: {
|
||||||
userId,
|
userId: authContext.userId,
|
||||||
orgId,
|
orgId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1320,12 +1409,12 @@ exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
|
|||||||
cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?canceled=true`,
|
cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?canceled=true`,
|
||||||
metadata: {
|
metadata: {
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
userId: authContext.userId,
|
||||||
},
|
},
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
userId: authContext.userId,
|
||||||
},
|
},
|
||||||
trial_period_days: 14, // 14-day trial
|
trial_period_days: 14, // 14-day trial
|
||||||
},
|
},
|
||||||
@@ -1412,13 +1501,14 @@ exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate auth token and get user context
|
||||||
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
|
|
||||||
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
|
}
|
||||||
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
||||||
|
|
||||||
if (!orgDoc.exists()) {
|
if (!orgDoc.exists()) {
|
||||||
@@ -1697,17 +1787,13 @@ exports.getOrgData = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
// Validate auth token and get user context
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
if (!isAuthorized) {
|
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get organization data
|
// Get organization data
|
||||||
@@ -1724,6 +1810,10 @@ exports.getOrgData = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get org data error:", error);
|
console.error("Get org data error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to get organization data" });
|
res.status(500).json({ error: "Failed to get organization data" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1739,17 +1829,18 @@ exports.updateOrgData = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId, data } = req.body;
|
|
||||||
|
|
||||||
if (!orgId || !userId || !data) {
|
|
||||||
return res.status(400).json({ error: "Organization ID, user ID, and data are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
// Validate auth token and get user context
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
if (!isAuthorized) {
|
const { data } = req.body;
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
||||||
|
if (!data) {
|
||||||
|
return res.status(400).json({ error: "Data is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update organization data
|
// Update organization data
|
||||||
@@ -1765,6 +1856,10 @@ exports.updateOrgData = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update org data error:", error);
|
console.error("Update org data error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to update organization data" });
|
res.status(500).json({ error: "Failed to update organization data" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1780,17 +1875,13 @@ exports.getEmployees = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
// Validate auth token and get user context
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
if (!isAuthorized) {
|
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all employees
|
// Get all employees
|
||||||
@@ -1807,6 +1898,10 @@ exports.getEmployees = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get employees error:", error);
|
console.error("Get employees error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to get employees" });
|
res.status(500).json({ error: "Failed to get employees" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1822,17 +1917,13 @@ exports.getSubmissions = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
// Validate auth token and get user context
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
if (!isAuthorized) {
|
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all submissions
|
// Get all submissions
|
||||||
@@ -1849,6 +1940,10 @@ exports.getSubmissions = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get submissions error:", error);
|
console.error("Get submissions error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to get submissions" });
|
res.status(500).json({ error: "Failed to get submissions" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1864,17 +1959,13 @@ exports.getReports = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
// Validate auth token and get user context
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
if (!isAuthorized) {
|
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
const orgId = authContext.orgId;
|
||||||
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all reports
|
// Get all reports
|
||||||
@@ -1891,6 +1982,10 @@ exports.getReports = onRequest({ cors: true }, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get reports error:", error);
|
console.error("Get reports error:", error);
|
||||||
|
if (error.message.includes('Missing or invalid authorization') ||
|
||||||
|
error.message.includes('Token')) {
|
||||||
|
return res.status(401).json({ error: error.message });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to get reports" });
|
res.status(500).json({ error: "Failed to get reports" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1906,6 +2001,7 @@ exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const { orgId, userId, employeeData } = req.body;
|
const { orgId, userId, employeeData } = req.body;
|
||||||
|
|
||||||
if (!orgId || !userId || !employeeData) {
|
if (!orgId || !userId || !employeeData) {
|
||||||
@@ -2007,17 +2103,12 @@ exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, userId } = req.query;
|
|
||||||
|
|
||||||
if (!orgId || !userId) {
|
|
||||||
return res.status(400).json({ error: "Organization ID and user ID are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify user authorization
|
const authContext = await validateAuthAndGetContext(req);
|
||||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
||||||
if (!isAuthorized) {
|
const orgId = authContext.orgId;
|
||||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
if (!orgId) {
|
||||||
|
return res.status(400).json({ error: "User has no associated organizations" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all company reports
|
// Get all company reports
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UserOrganizationsProvider, useUserOrganizations } from './contexts/User
|
|||||||
import { OrgProvider, useOrg } from './contexts/OrgContext';
|
import { OrgProvider, useOrg } from './contexts/OrgContext';
|
||||||
import { Layout } from './components/UiKit';
|
import { Layout } from './components/UiKit';
|
||||||
import CompanyWiki from './pages/CompanyWiki';
|
import CompanyWiki from './pages/CompanyWiki';
|
||||||
import EmployeeReport from './pages/EmployeeData';
|
// import Report from '../deprecated/pages/EmployeeData';
|
||||||
import Reports from './pages/Reports';
|
import Reports from './pages/Reports';
|
||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
import HelpNew from './pages/HelpNew';
|
import HelpNew from './pages/HelpNew';
|
||||||
@@ -257,7 +257,10 @@ function App() {
|
|||||||
>
|
>
|
||||||
<Route path="/" element={<Navigate to="/reports" replace />} />
|
<Route path="/" element={<Navigate to="/reports" replace />} />
|
||||||
<Route path="/company-wiki" element={<CompanyWiki />} />
|
<Route path="/company-wiki" element={<CompanyWiki />} />
|
||||||
<Route path="/submissions" element={<EmployeeReport mode="submissions" />} />
|
{/*
|
||||||
|
This is the old reports page
|
||||||
|
<Route path="/submissions" element={<Report mode="submissions" />} />
|
||||||
|
*/}
|
||||||
<Route path="/reports" element={<Reports />} />
|
<Route path="/reports" element={<Reports />} />
|
||||||
<Route path="/help" element={<HelpAndSettings />} />
|
<Route path="/help" element={<HelpAndSettings />} />
|
||||||
<Route path="/settings" element={<HelpAndSettings />} />
|
<Route path="/settings" element={<HelpAndSettings />} />
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ export const CompanyWikiCompletedState: React.FC<CompanyWikiCompletedStateProps>
|
|||||||
onClick={() => onSectionClick?.(sectionNumber)}
|
onClick={() => onSectionClick?.(sectionNumber)}
|
||||||
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer hover:bg-Main-BG-Gray-50 dark:hover:bg-[--Neutrals-NeutralSlate700] ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-[--Neutrals-NeutralSlate700] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : ''}`}
|
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer hover:bg-Main-BG-Gray-50 dark:hover:bg-[--Neutrals-NeutralSlate700] ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-[--Neutrals-NeutralSlate700] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : ''}`}
|
||||||
>
|
>
|
||||||
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-[--Brand-Orange]' : 'bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate600'}`}>
|
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-[--Brand-Orange]' : 'bg-Text-Gray-100 dark:bg-[--Neutrals-NeutralSlate600]'}`}>
|
||||||
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate0' : 'text-Text-Dark-950 dark:text-Neutrals-NeutralSlate200'}`}>
|
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate0]' : 'text-Text-Dark-950 dark:text-[--Neutrals-NeutralSlate0]'}`}>
|
||||||
{sectionNumber}
|
{sectionNumber}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate800] dark:text-Neutrals-NeutralSlate100' : 'text-Text-Gray-500 dark:text-Neutrals-NeutralSlate400'}`}>
|
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate800] dark:text-[--Neutrals-NeutralSlate100]' : 'text-Text-Gray-500 dark:text-[--Neutrals-NeutralSlate100]'}`}>
|
||||||
{section}
|
{section}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ export const CompanyWikiEmptyState: React.FC<CompanyWikiEmptyStateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-[--Neutrals-NeutralSlate800] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : 'hover:bg-Main-BG-Gray-50 dark:hover:bg-Neutrals-NeutralSlate700'}`}
|
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-[--Neutrals-NeutralSlate800] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : 'hover:bg-Main-BG-Gray-50 dark:hover:bg-[--Neutrals-NeutralSlate700]'}`}
|
||||||
>
|
>
|
||||||
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-[--Brand-Orange]' : 'bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate600'}`}>
|
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-[--Brand-Orange]' : 'bg-Text-Gray-100 dark:bg-[--Neutrals-NeutralSlate600]'}`}>
|
||||||
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate0' : 'text-Text-Dark-950 dark:text-Neutrals-NeutralSlate200'}`}>
|
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate0]' : 'text-Text-Dark-950 dark:text-[--Neutrals-NeutralSlate0]'}`}>
|
||||||
{sectionNumber}
|
{sectionNumber}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate800] dark:text-Neutrals-NeutralSlate100' : 'text-Text-Gray-500 dark:text-Neutrals-NeutralSlate400'}`}>
|
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-[--Neutrals-NeutralSlate800] dark:text-[--Neutrals-NeutralSlate100]' : 'text-Text-Gray-500 dark:text-[--Neutrals-NeutralSlate100]'}`}>
|
||||||
{section}
|
{section}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const InviteEmployeesModal: React.FC<InviteEmployeesModalProps> = ({
|
|||||||
<div className="self-stretch p-6 inline-flex justify-between items-center">
|
<div className="self-stretch p-6 inline-flex justify-between items-center">
|
||||||
<button
|
<button
|
||||||
onClick={onMultipleInvite}
|
onClick={onMultipleInvite}
|
||||||
className="text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight hover:underline"
|
className="text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight hover:underline"
|
||||||
>
|
>
|
||||||
Invite multiple employees
|
Invite multiple employees
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[464px] max-w-[464px] min-w-[464px] absolute top-[24px] left-1/2 transform -translate-x-1/2 flex flex-col justify-start items-center gap-4">
|
<div className="w-[464px] max-w-[464px] min-w-[464px] absolute top-[24px] left-1/2 transform -translate-x-1/2 flex flex-col justify-start items-center gap-4">
|
||||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
{Array.from({ length: 7 }, (_, index) => {
|
{Array.from({ length: 7 }, (_, index) => {
|
||||||
const isActive = index === currentSection - 1;
|
const isActive = index === currentSection - 1;
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +44,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate500] text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{sectionName}
|
{sectionName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,27 +58,27 @@ export const WelcomeScreen: React.FC<{
|
|||||||
}> = ({ onStart, imageUrl = "https://placehold.co/560x682" }) => {
|
}> = ({ onStart, imageUrl = "https://placehold.co/560x682" }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
<div className="w-12 h-12 relative bg-[--Brand-Orange] rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
<div className="left-[12px] top-[9.33px] absolute">
|
||||||
<AuditlyIcon />
|
<AuditlyIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||||
Welcome to the Internal Staff Survey
|
Welcome to the Internal Staff Survey
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal">
|
||||||
The survey takes around 15 minutes to complete.
|
The survey takes around 15 minutes to complete.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||||
@@ -105,32 +105,32 @@ export const SectionIntro: React.FC<{
|
|||||||
}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => {
|
}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
<div className="w-12 h-12 relative bg-[--Brand-Orange] rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
<div className="left-[12px] top-[9.33px] absolute">
|
||||||
<AuditlyIcon />
|
<AuditlyIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
<div className="px-3 py-1.5 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{sectionNumber}
|
{sectionNumber}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||||
@@ -159,20 +159,20 @@ export const FormField: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
{required && (
|
{required && (
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,10 +190,10 @@ export const PersonalInfoForm: React.FC<{
|
|||||||
const isValid = formData.email && formData.name && formData.company;
|
const isValid = formData.email && formData.name && formData.company;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] px-[488px] py-32 bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] px-[488px] py-32 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
Personal Information
|
Personal Information
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
@@ -224,7 +224,7 @@ export const PersonalInfoForm: React.FC<{
|
|||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="self-stretch h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
className="self-stretch h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -249,17 +249,17 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, placeholder = "Type your answer...." }) => {
|
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, placeholder = "Type your answer...." }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
<div className="self-stretch min-h-40 p-5 relative bg-[--Neutrals-NeutralSlate100] rounded-xl inline-flex justify-start items-start gap-2.5">
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-base font-normal font-['Inter'] leading-normal placeholder:text-[--Neutrals-NeutralSlate950] outline-none resize-none"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
@@ -273,16 +273,16 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -293,8 +293,8 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
|
|
||||||
{/* Step indicator */}
|
{/* Step indicator */}
|
||||||
{currentStep && totalSteps && (
|
{currentStep && totalSteps && (
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{currentStep} of {totalSteps}
|
{currentStep} of {totalSteps}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,9 +304,9 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
{onSkip && (
|
{onSkip && (
|
||||||
<button
|
<button
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -338,14 +338,14 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
scale?: number;
|
scale?: number;
|
||||||
}> = ({ question, leftLabel, rightLabel, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, scale = 10 }) => {
|
}> = ({ question, leftLabel, rightLabel, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, scale = 10 }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-center items-center gap-8">
|
<div className="self-stretch flex flex-col justify-center items-center gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex justify-center items-center gap-3">
|
<div className="inline-flex justify-center items-center gap-3">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||||
{leftLabel}
|
{leftLabel}
|
||||||
</div>
|
</div>
|
||||||
{Array.from({ length: scale }, (_, index) => {
|
{Array.from({ length: scale }, (_, index) => {
|
||||||
@@ -355,19 +355,17 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
<button
|
<button
|
||||||
key={ratingValue}
|
key={ratingValue}
|
||||||
onClick={() => onChange(ratingValue)}
|
onClick={() => onChange(ratingValue)}
|
||||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${
|
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
|
||||||
isSelected ? 'bg-Neutrals-NeutralSlate800' : 'bg-Neutrals-NeutralSlate100 hover:bg-neutral-200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${
|
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
|
||||||
isSelected ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
}`}>
|
||||||
}`}>
|
|
||||||
{ratingValue}
|
{ratingValue}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||||
{rightLabel}
|
{rightLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,17 +374,17 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -397,8 +395,8 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
|
|
||||||
{/* Step indicator */}
|
{/* Step indicator */}
|
||||||
{currentStep && totalSteps && (
|
{currentStep && totalSteps && (
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{currentStep} of {totalSteps}
|
{currentStep} of {totalSteps}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -408,9 +406,9 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
{onSkip && (
|
{onSkip && (
|
||||||
<button
|
<button
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -439,34 +437,30 @@ export const YesNoChoice: React.FC<{
|
|||||||
sectionName?: string;
|
sectionName?: string;
|
||||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('No')}
|
onClick={() => onChange('No')}
|
||||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${
|
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
|
||||||
value === 'No' ? 'bg-Neutrals-NeutralSlate800' : 'bg-Neutrals-NeutralSlate100 hover:bg-neutral-200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${
|
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
|
||||||
value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
}`}>
|
||||||
}`}>
|
|
||||||
No
|
No
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('Yes')}
|
onClick={() => onChange('Yes')}
|
||||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${
|
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
|
||||||
value === 'Yes' ? 'bg-Neutrals-NeutralSlate800' : 'bg-Neutrals-NeutralSlate100 hover:bg-neutral-200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${
|
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
|
||||||
value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
}`}>
|
||||||
}`}>
|
|
||||||
Yes
|
Yes
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -476,17 +470,17 @@ export const YesNoChoice: React.FC<{
|
|||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -497,8 +491,8 @@ export const YesNoChoice: React.FC<{
|
|||||||
|
|
||||||
{/* Step indicator */}
|
{/* Step indicator */}
|
||||||
{currentStep && totalSteps && (
|
{currentStep && totalSteps && (
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{currentStep} of {totalSteps}
|
{currentStep} of {totalSteps}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -508,9 +502,9 @@ export const YesNoChoice: React.FC<{
|
|||||||
{onSkip && (
|
{onSkip && (
|
||||||
<button
|
<button
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-neutral-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -530,20 +524,20 @@ export const YesNoChoice: React.FC<{
|
|||||||
export const ThankYouPage: React.FC = () => {
|
export const ThankYouPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
<div className="w-12 h-12 relative bg-[--Brand-Orange] rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
<div className="left-[12px] top-[9.33px] absolute">
|
||||||
<AuditlyIcon />
|
<AuditlyIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||||
Thank you your form has been submitted!
|
Thank you your form has been submitted!
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal">
|
||||||
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,10 +167,10 @@ export const FigmaRatingScale: React.FC<{
|
|||||||
<div
|
<div
|
||||||
key={number}
|
key={number}
|
||||||
onClick={() => onChange(number)}
|
onClick={() => onChange(number)}
|
||||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden cursor-pointer transition-colors ${isSelected ? 'bg-[--Brand-Orange]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-Neutrals-NeutralSlate200'
|
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden cursor-pointer transition-colors ${isSelected ? 'bg-[--Brand-Orange]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate200]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-white' : 'text-Neutrals-NeutralSlate950'
|
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-white' : 'text-[--Neutrals-NeutralSlate950]'
|
||||||
}`}>
|
}`}>
|
||||||
{number}
|
{number}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,46 +16,81 @@ export const FigmaOnboardingIntro: React.FC<FigmaOnboardingIntroProps> = ({
|
|||||||
onStart
|
onStart
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
<div className="w-[1440px] bg-transparent inline-flex justify-start items-center overflow-hidden">
|
||||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
<div className="w-12 h-12 relative bg-[--Brand-Orange] rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
<div data-svg-wrapper className="left-[12px] top-[9.33px] absolute">
|
||||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8128C3.11835 18.3639 3.11834 19.2575 2.57406 19.8087L2.54619 19.8369C2.00191 20.3881 1.11946 20.3881 0.57519 19.8369C0.030919 19.2857 0.0309274 18.3921 0.575208 17.841L0.603083 17.8128C1.14736 17.2616 2.02981 17.2616 2.57408 17.8128Z" fill="url(#paint0_linear_755_3218)" />
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M2.57408 17.8128C3.11835 18.3639 3.11834 19.2575 2.57406 19.8087L2.54619 19.8369C2.00191 20.3881 1.11946 20.3881 0.57519 19.8369C0.030919 19.2857 0.0309274 18.3921 0.575208 17.841L0.603083 17.8128C1.14736 17.2616 2.02981 17.2616 2.57408 17.8128Z" fill="url(#paint0_linear_759_16243)" />
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2379C9.66912 18.7901 9.66752 19.6837 9.12226 20.2338L5.2617 24.1291C4.71644 24.6792 3.83399 24.6776 3.2907 24.1255C2.74741 23.5733 2.74901 22.6797 3.29427 22.1296L7.15483 18.2343C7.70009 17.6842 8.58254 17.6858 9.12583 18.2379Z" fill="url(#paint1_linear_755_3218)" />
|
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M9.12583 18.2379C9.66912 18.7901 9.66752 19.6837 9.12226 20.2338L5.2617 24.1291C4.71644 24.6792 3.83399 24.6776 3.2907 24.1255C2.74741 23.5733 2.74901 22.6797 3.29427 22.1296L7.15483 18.2343C7.70009 17.6842 8.58254 17.6858 9.12583 18.2379Z" fill="url(#paint1_linear_759_16243)" />
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.3656 24.9431C14.7925 24.2945 15.6578 24.1193 16.2983 24.5516L16.3819 24.6081C17.0224 25.0404 17.1954 25.9167 16.7685 26.5652C16.3415 27.2138 15.4762 27.389 14.8357 26.9567L14.7521 26.9002C14.1117 26.4678 13.9386 25.5916 14.3656 24.9431Z" fill="url(#paint2_linear_759_16243)" />
|
||||||
|
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M23.5637 17.7278C24.108 18.279 24.108 19.1726 23.5637 19.7237L21.6683 21.6431C21.124 22.1943 20.2415 22.1943 19.6973 21.6431C19.153 21.092 19.153 20.1984 19.6973 19.6472L21.5927 17.7278C22.137 17.1767 23.0194 17.1767 23.5637 17.7278Z" fill="url(#paint3_linear_759_16243)" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.592 10.6292C24.1363 11.1803 24.1363 12.0739 23.592 12.6251L9.58526 26.8089C9.04098 27.36 8.15854 27.36 7.61426 26.8089C7.06999 26.2577 7.06999 25.3641 7.61426 24.8129L21.621 10.6292C22.1653 10.078 23.0477 10.078 23.592 10.6292Z" fill="url(#paint4_linear_759_16243)" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9426 6.26835C21.4869 6.8195 21.4869 7.7131 20.9426 8.26425L12.887 16.4217C12.3427 16.9728 11.4603 16.9728 10.916 16.4217C10.3717 15.8705 10.3717 14.9769 10.916 14.4258L18.9716 6.26835C19.5159 5.71719 20.3984 5.71719 20.9426 6.26835Z" fill="url(#paint5_linear_759_16243)" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6918 3.50156C17.2364 4.05235 17.237 4.94594 16.6931 5.49747L6.33787 15.9977C5.79396 16.5492 4.91152 16.5498 4.36688 15.999C3.82224 15.4482 3.82164 14.5546 4.36555 14.0031L14.7208 3.5029C15.2647 2.95138 16.1471 2.95078 16.6918 3.50156Z" fill="url(#paint6_linear_759_16243)" />
|
||||||
|
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M6.43658 6.86284C6.97992 7.41494 6.9784 8.30854 6.43319 8.85874L2.37751 12.9516C1.83229 13.5018 0.94985 13.5002 0.406512 12.9481C-0.136826 12.396 -0.135307 11.5024 0.409905 10.9522L4.46559 6.8594C5.0108 6.3092 5.89324 6.31074 6.43658 6.86284Z" fill="url(#paint7_linear_759_16243)" />
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M8.92007 2.77832C9.6898 2.77832 10.3138 3.41019 10.3138 4.18964V4.33077C10.3138 5.11022 9.6898 5.74209 8.92007 5.74209C8.15035 5.74209 7.52637 5.11022 7.52637 4.33077V4.18964C7.52637 3.41019 8.15035 2.77832 8.92007 2.77832Z" fill="url(#paint8_linear_759_16243)" />
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="paint0_linear_755_3218" x1="1.57463" y1="17.3994" x2="1.57463" y2="20.2503" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint0_linear_759_16243" x1="1.57463" y1="17.3994" x2="1.57463" y2="20.2503" gradientUnits="userSpaceOnUse">
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="paint1_linear_755_3218" x1="6.20827" y1="17.8228" x2="6.20827" y2="24.5406" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint1_linear_759_16243" x1="6.20827" y1="17.8228" x2="6.20827" y2="24.5406" gradientUnits="userSpaceOnUse">
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_759_16243" x1="15.567" y1="24.3145" x2="15.567" y2="27.1938" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_759_16243" x1="21.6305" y1="17.3145" x2="21.6305" y2="22.0565" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear_759_16243" x1="15.6031" y1="10.2158" x2="15.6031" y2="27.2222" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear_759_16243" x1="15.9293" y1="5.85498" x2="15.9293" y2="16.835" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint6_linear_759_16243" x1="10.5293" y1="3.08887" x2="10.5293" y2="16.4117" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint7_linear_759_16243" x1="3.42155" y1="6.44775" x2="3.42155" y2="13.3632" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint8_linear_759_16243" x1="8.92007" y1="2.77832" x2="8.92007" y2="5.74209" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.5" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
<div className="px-3 py-1.5 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{section} of {totalSections}
|
{section} of {totalSections}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-Brand-Orange/90 transition-colors"
|
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-[--Brand-Orange]/90 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||||
@@ -63,9 +98,9 @@ export const FigmaOnboardingIntro: React.FC<FigmaOnboardingIntroProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
<div className="self-stretch h-[810px] px-20 py-16 inline-flex justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||||
<img className="self-stretch flex-1 object-cover" src="https://placehold.co/560x682" alt="Onboarding illustration" />
|
<img className="self-stretch flex-1" src="/public/image/onboarding-robot.png" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,17 +158,17 @@ export const FigmaOnboardingQuestion: React.FC<FigmaOnboardingQuestionProps> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-36">
|
<div className="w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-36">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
<div className="self-stretch min-h-40 p-5 relative bg-[--Neutrals-NeutralSlate100] rounded-xl inline-flex justify-start items-start gap-2.5">
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-base font-normal font-['Inter'] leading-normal placeholder:text-[--Neutrals-NeutralSlate950] outline-none resize-none"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
/>
|
/>
|
||||||
@@ -146,16 +181,16 @@ export const FigmaOnboardingQuestion: React.FC<FigmaOnboardingQuestionProps> = (
|
|||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200 transition-colors"
|
className="h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-[--Neutrals-NeutralSlate100] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-Brand-Orange/90 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[--Brand-Orange]/90 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -164,23 +199,23 @@ export const FigmaOnboardingQuestion: React.FC<FigmaOnboardingQuestionProps> = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{sectionPosition} of {totalInSection}
|
{sectionPosition} of {totalInSection}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canSkip && (
|
{canSkip && (
|
||||||
<div className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200 transition-colors">
|
<div className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100] transition-colors">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
{renderProgressDots()}
|
{renderProgressDots()}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate500] text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{sectionName}
|
{sectionName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +254,7 @@ export const FigmaOnboardingMultipleChoice: React.FC<FigmaOnboardingMultipleChoi
|
|||||||
for (let i = 1; i <= 7; i++) {
|
for (let i = 1; i <= 7; i++) {
|
||||||
if (i === sectionPosition) {
|
if (i === sectionPosition) {
|
||||||
dots.push(
|
dots.push(
|
||||||
<div key={i} className="w-6 h-1 bg-Brand-Orange rounded-3xl" />
|
<div key={i} className="w-6 h-1 bg-[--Brand-Orange] rounded-3xl" />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dots.push(
|
dots.push(
|
||||||
@@ -233,10 +268,10 @@ export const FigmaOnboardingMultipleChoice: React.FC<FigmaOnboardingMultipleChoi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||||
@@ -244,17 +279,15 @@ export const FigmaOnboardingMultipleChoice: React.FC<FigmaOnboardingMultipleChoi
|
|||||||
<button
|
<button
|
||||||
key={option}
|
key={option}
|
||||||
onClick={() => onSelect(option)}
|
onClick={() => onSelect(option)}
|
||||||
className={`flex-1 h-20 relative rounded-[999px] overflow-hidden transition-colors ${
|
className={`flex-1 h-20 relative rounded-[999px] overflow-hidden transition-colors ${selectedValue === option
|
||||||
selectedValue === option
|
? 'bg-[--Neutrals-NeutralSlate800]'
|
||||||
? 'bg-Neutrals-NeutralSlate800'
|
: 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate50]'
|
||||||
: 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${selectedValue === option
|
||||||
selectedValue === option
|
? 'text-[--Neutrals-NeutralSlate0]'
|
||||||
? 'text-Neutrals-NeutralSlate0'
|
: 'text-[--Neutrals-NeutralSlate950]'
|
||||||
: 'text-Neutrals-NeutralSlate950'
|
}`}>
|
||||||
}`}>
|
|
||||||
{option}
|
{option}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -264,16 +297,16 @@ export const FigmaOnboardingMultipleChoice: React.FC<FigmaOnboardingMultipleChoi
|
|||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200 transition-colors"
|
className="h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-[--Neutrals-NeutralSlate100] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!selectedValue}
|
disabled={!selectedValue}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-Brand-Orange/90 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[--Brand-Orange]/90 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
@@ -282,23 +315,23 @@ export const FigmaOnboardingMultipleChoice: React.FC<FigmaOnboardingMultipleChoi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||||
{sectionPosition} of {totalInSection}
|
{sectionPosition} of {totalInSection}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canSkip && (
|
{canSkip && (
|
||||||
<div className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200 transition-colors">
|
<div className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100] transition-colors">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
{renderProgressDots()}
|
{renderProgressDots()}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate500] text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{sectionName}
|
{sectionName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,16 +368,16 @@ export const FigmaOnboardingForm: React.FC<FigmaOnboardingFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[810px] px-[488px] py-32 bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
<div className="w-[1440px] h-[810px] px-[488px] py-32 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
Company Details
|
Company Details
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight">Company Logo</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight">Company Logo</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-4 rounded-3xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-start items-center gap-4">
|
<div className="self-stretch p-4 rounded-3xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-start items-center gap-4">
|
||||||
<div className="w-16 h-16 relative bg-gray-200 rounded-[250px]">
|
<div className="w-16 h-16 relative bg-gray-200 rounded-[250px]">
|
||||||
@@ -361,7 +394,7 @@ export const FigmaOnboardingForm: React.FC<FigmaOnboardingFormProps> = ({
|
|||||||
<path d="M14 2H2M12 8.66667L8 4.66667M8 4.66667L4 8.66667M8 4.66667V14" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M14 2H2M12 8.66667L8 4.66667M8 4.66667L4 8.66667M8 4.66667V14" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Upload image</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload image</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -375,16 +408,16 @@ export const FigmaOnboardingForm: React.FC<FigmaOnboardingFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={yourName}
|
value={yourName}
|
||||||
onChange={(e) => onYourNameChange(e.target.value)}
|
onChange={(e) => onYourNameChange(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -392,16 +425,16 @@ export const FigmaOnboardingForm: React.FC<FigmaOnboardingFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">Company Name</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Company Name</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={companyName}
|
value={companyName}
|
||||||
onChange={(e) => onCompanyNameChange(e.target.value)}
|
onChange={(e) => onCompanyNameChange(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none"
|
||||||
placeholder="Doe Enterprises"
|
placeholder="Doe Enterprises"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,7 +445,7 @@ export const FigmaOnboardingForm: React.FC<FigmaOnboardingFormProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
className="self-stretch h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-Brand-Orange/90 transition-colors"
|
className="self-stretch h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[--Brand-Orange]/90 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||||||
|
|
||||||
{dragOver && (
|
{dragOver && (
|
||||||
<div className="absolute inset-0 bg-[--Brand-Orange] bg-opacity-20 flex items-center justify-center">
|
<div className="absolute inset-0 bg-[--Brand-Orange] bg-opacity-20 flex items-center justify-center">
|
||||||
<span className="text-Brand-Orange text-xs font-medium">Drop image</span>
|
<span className="text-[--Brand-Orange] text-xs font-medium">Drop image</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
174
src/constants.ts
174
src/constants.ts
@@ -1,4 +1,4 @@
|
|||||||
import { Employee, Report, Submission, FaqItem, CompanyReport } from './types';
|
import { Employee, EmployeeReport, Submission, FaqItem, CompanyReport } from './types';
|
||||||
|
|
||||||
// URL Configuration - reads from environment variables with fallbacks
|
// URL Configuration - reads from environment variables with fallbacks
|
||||||
export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173';
|
export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173';
|
||||||
@@ -24,7 +24,7 @@ if (import.meta.env.DEV) {
|
|||||||
export const EMPLOYEES: Employee[] = [];
|
export const EMPLOYEES: Employee[] = [];
|
||||||
|
|
||||||
// DEPRECATED: Sample report data - real reports should be generated via /generateEmployeeReport API
|
// DEPRECATED: Sample report data - real reports should be generated via /generateEmployeeReport API
|
||||||
export const REPORT_DATA: Report = {
|
export const REPORT_DATA: EmployeeReport = {
|
||||||
employeeId: 'sample',
|
employeeId: 'sample',
|
||||||
department: 'Sample Department',
|
department: 'Sample Department',
|
||||||
role: 'Sample Role',
|
role: 'Sample Role',
|
||||||
@@ -45,11 +45,14 @@ export const REPORT_DATA: Report = {
|
|||||||
weaknesses: [
|
weaknesses: [
|
||||||
{ isCritical: false, description: 'Sample data - use AI-generated reports' }
|
{ isCritical: false, description: 'Sample data - use AI-generated reports' }
|
||||||
],
|
],
|
||||||
opportunities: {
|
opportunities: [
|
||||||
roleAdjustment: 'Sample data',
|
{
|
||||||
accountabilitySupport: 'Sample data',
|
roleAdjustment: 'Sample data',
|
||||||
},
|
accountabilitySupport: 'Sample data',
|
||||||
|
}
|
||||||
|
],
|
||||||
risks: ['Sample data - use AI-generated reports'],
|
risks: ['Sample data - use AI-generated reports'],
|
||||||
|
recommendations: ['Sample recommendation - use AI-generated reports'],
|
||||||
recommendation: {
|
recommendation: {
|
||||||
action: 'Keep',
|
action: 'Keep',
|
||||||
details: ['Sample data - use AI-generated reports'],
|
details: ['Sample data - use AI-generated reports'],
|
||||||
@@ -109,22 +112,159 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
id: 'placeholder-report',
|
id: 'placeholder-report',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
overview: {
|
overview: {
|
||||||
totalEmployees: 0,
|
totalEmployees: 8,
|
||||||
departmentBreakdown: [],
|
departmentBreakdown: [
|
||||||
submissionRate: 0,
|
{ department: 'Campaigns', count: 3 },
|
||||||
|
{ department: 'Social Media', count: 2 },
|
||||||
|
{ department: 'Creative', count: 2 },
|
||||||
|
{ department: 'Tech', count: 1 }
|
||||||
|
],
|
||||||
|
submissionRate: 87.5,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
averagePerformanceScore: 0,
|
averagePerformanceScore: 8.2,
|
||||||
riskLevel: 'Low'
|
riskLevel: 'Low'
|
||||||
},
|
},
|
||||||
gradingBreakdown: [],
|
weaknesses: [
|
||||||
|
{ title: 'Lack of Structure', description: 'People are confused about their roles. No one knows who\'s responsible.' },
|
||||||
|
{ title: 'Communication Gaps', description: 'Information doesn\'t flow effectively between departments.' }
|
||||||
|
],
|
||||||
|
gradingBreakdown: {
|
||||||
|
departmentNameShort: 'Campaigns',
|
||||||
|
departmentName: 'Campaigns Department',
|
||||||
|
lead: 'Sarah Johnson',
|
||||||
|
support: 'Mike Chen',
|
||||||
|
departmentGrade: 'A',
|
||||||
|
executiveSummary: 'Campaigns team shows exceptional performance with strong collaboration and innovative approaches to client work.',
|
||||||
|
teamScores: [
|
||||||
|
{
|
||||||
|
employeeName: 'Sarah Johnson',
|
||||||
|
grade: 'A+',
|
||||||
|
reliability: 9,
|
||||||
|
roleFit: 10,
|
||||||
|
scalability: 8,
|
||||||
|
output: 9,
|
||||||
|
initiative: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Mike Chen',
|
||||||
|
grade: 'A',
|
||||||
|
reliability: 8,
|
||||||
|
roleFit: 9,
|
||||||
|
scalability: 7,
|
||||||
|
output: 8,
|
||||||
|
initiative: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Lisa Wong',
|
||||||
|
grade: 'B+',
|
||||||
|
reliability: 7,
|
||||||
|
roleFit: 8,
|
||||||
|
scalability: 6,
|
||||||
|
output: 7,
|
||||||
|
initiative: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Alex Rivera',
|
||||||
|
grade: 'A',
|
||||||
|
reliability: 8,
|
||||||
|
roleFit: 9,
|
||||||
|
scalability: 7,
|
||||||
|
output: 8,
|
||||||
|
initiative: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Jamie Park',
|
||||||
|
grade: 'B',
|
||||||
|
reliability: 6,
|
||||||
|
roleFit: 7,
|
||||||
|
scalability: 6,
|
||||||
|
output: 6,
|
||||||
|
initiative: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Taylor Smith',
|
||||||
|
grade: 'A',
|
||||||
|
reliability: 9,
|
||||||
|
roleFit: 9,
|
||||||
|
scalability: 8,
|
||||||
|
output: 9,
|
||||||
|
initiative: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Jordan Lee',
|
||||||
|
grade: 'B+',
|
||||||
|
reliability: 7,
|
||||||
|
roleFit: 8,
|
||||||
|
scalability: 7,
|
||||||
|
output: 8,
|
||||||
|
initiative: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Casey Johnson',
|
||||||
|
grade: 'A+',
|
||||||
|
reliability: 10,
|
||||||
|
roleFit: 9,
|
||||||
|
scalability: 9,
|
||||||
|
output: 10,
|
||||||
|
initiative: 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] },
|
operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] },
|
||||||
personnelChanges: { newHires: [], promotions: [], departures: [] },
|
personnelChanges: {
|
||||||
|
newHires: [
|
||||||
|
{ name: 'Emma Wilson', department: 'Creative', role: 'Graphic Designer', impact: 'Will strengthen visual design capabilities' }
|
||||||
|
],
|
||||||
|
promotions: [
|
||||||
|
{ name: 'Sarah Johnson', fromRole: 'Campaign Manager', toRole: 'Senior Campaign Manager', impact: 'Will lead larger client accounts' }
|
||||||
|
],
|
||||||
|
departures: []
|
||||||
|
},
|
||||||
keyPersonnelChanges: [],
|
keyPersonnelChanges: [],
|
||||||
immediateHiringNeeds: [],
|
immediateHiringNeeds: [
|
||||||
forwardOperatingPlan: {
|
{ department: 'Tech', role: 'Frontend Developer', priority: 'High', reasoning: 'Need to expand web development capacity for growing client demands' },
|
||||||
quarterlyGoals: [],
|
{ department: 'Social Media', role: 'Content Creator', priority: 'Medium', reasoning: 'Additional support needed for video content production' }
|
||||||
resourceNeeds: [],
|
],
|
||||||
riskMitigation: []
|
recommendations: [
|
||||||
|
'Hire additional frontend developer for tech team expansion',
|
||||||
|
'Implement cross-departmental collaboration sessions',
|
||||||
|
'Develop leadership training program for senior staff',
|
||||||
|
'Establish mentorship program for junior employees'
|
||||||
|
],
|
||||||
|
forwardOperatingPlan: [
|
||||||
|
{
|
||||||
|
title: 'Q1 Client Growth Initiative',
|
||||||
|
details: ['Expand campaign team by 2 members', 'Implement new project management system', 'Launch client referral program']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Technology Infrastructure',
|
||||||
|
details: ['Upgrade development tools', 'Implement automated testing', 'Enhance security protocols']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
strengths: [
|
||||||
|
'Strong creative vision and execution across all projects',
|
||||||
|
'Excellent client satisfaction ratings (4.8/5)',
|
||||||
|
'Highly collaborative team culture',
|
||||||
|
'Innovative approach to campaign development',
|
||||||
|
'Reliable project delivery within deadlines'
|
||||||
|
],
|
||||||
|
organizationalImpactSummary: {
|
||||||
|
missionCritical: [
|
||||||
|
{ employeeName: 'Sarah Johnson', impact: 'Lead Campaign Manager', description: 'Manages top-tier client relationships and drives 40% of company revenue' },
|
||||||
|
{ employeeName: 'Casey Johnson', impact: 'Tech Infrastructure', description: 'Maintains all technical systems and client delivery platforms' }
|
||||||
|
],
|
||||||
|
highlyValuable: [
|
||||||
|
{ employeeName: 'Alex Rivera', impact: 'Social Media Strategy', description: 'Develops social strategies that increase client engagement by 150%' },
|
||||||
|
{ employeeName: 'Taylor Smith', impact: 'Creative Director', description: 'Ensures brand consistency and award-winning creative output' }
|
||||||
|
],
|
||||||
|
coreSupport: [
|
||||||
|
{ employeeName: 'Mike Chen', impact: 'Campaign Support', description: 'Reliable execution of campaign tasks and client communication' },
|
||||||
|
{ employeeName: 'Jordan Lee', impact: 'Design Support', description: 'Produces high-quality visual assets and maintains design standards' }
|
||||||
|
],
|
||||||
|
lowCriticality: [
|
||||||
|
{ employeeName: 'Jamie Park', impact: 'Content Assistant', description: 'Supports content creation with room for skill development' },
|
||||||
|
{ employeeName: 'Morgan Davis', impact: 'Administrative Support', description: 'Handles routine operations and documentation' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
organizationalStrengths: [],
|
organizationalStrengths: [],
|
||||||
organizationalRisks: [],
|
organizationalRisks: [],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { Employee, Report, Submission, CompanyReport } from '../types';
|
import { Employee, EmployeeReport, Submission, CompanyReport } from '../types';
|
||||||
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||||
import { apiPost, apiPut } from '../services/api';
|
import { apiPost, apiPut } from '../services/api';
|
||||||
import { User } from 'firebase/auth';
|
import { User } from 'firebase/auth';
|
||||||
@@ -9,29 +9,10 @@ import { secureApi } from '../services/secureApi';
|
|||||||
|
|
||||||
interface OrgData {
|
interface OrgData {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
name: string;
|
companyName?: string;
|
||||||
industry?: string;
|
onboardingData?: Record<string, any>;
|
||||||
size?: string;
|
companyLogo?: string;
|
||||||
description?: string;
|
updatedAt?: number;
|
||||||
mission?: string;
|
|
||||||
vision?: string;
|
|
||||||
values?: string;
|
|
||||||
foundingYear?: string;
|
|
||||||
evolution?: string;
|
|
||||||
majorMilestones?: string;
|
|
||||||
advantages?: string;
|
|
||||||
vulnerabilities?: string;
|
|
||||||
competitors?: string;
|
|
||||||
marketPosition?: string;
|
|
||||||
currentChallenges?: string;
|
|
||||||
shortTermGoals?: string;
|
|
||||||
longTermGoals?: string;
|
|
||||||
keyMetrics?: string;
|
|
||||||
cultureDescription?: string;
|
|
||||||
workEnvironment?: string;
|
|
||||||
leadershipStyle?: string;
|
|
||||||
communicationStyle?: string;
|
|
||||||
additionalContext?: string;
|
|
||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,16 +22,16 @@ interface OrgContextType {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
employees: Employee[];
|
employees: Employee[];
|
||||||
submissions: Record<string, Submission>;
|
submissions: Record<string, Submission>;
|
||||||
reports: Record<string, Report>;
|
reports: Record<string, EmployeeReport>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
||||||
saveReport: (employeeId: string, report: Report) => Promise<void>;
|
saveReport: (employeeId: string, report: EmployeeReport) => Promise<void>;
|
||||||
inviteEmployee: (args: { name: string; email: string }) => Promise<{ employeeId: string; inviteLink: string }>;
|
inviteEmployee: (args: { name: string; email: string }) => Promise<{ employeeId: string; inviteLink: string }>;
|
||||||
issueInviteViaApi: (args: { name: string; email: string; role?: string; department?: string }) => Promise<{ code: string; inviteLink: string; emailLink: string; employee: any }>;
|
issueInviteViaApi: (args: { name: string; email: string; role?: string; department?: string }) => Promise<{ code: string; inviteLink: string; emailLink: string; employee: any }>;
|
||||||
getInviteStatus: (code: string) => Promise<{ used: boolean; employee: any } | null>;
|
getInviteStatus: (code: string) => Promise<{ used: boolean; employee: any } | null>;
|
||||||
consumeInvite: (code: string) => Promise<{ employee: any; orgId?: string } | null>;
|
consumeInvite: (code: string) => Promise<{ employee: any; orgId?: string } | null>;
|
||||||
getReportVersions: (employeeId: string) => Promise<Array<{ id: string; createdAt: number; report: Report }>>;
|
getReportVersions: (employeeId: string) => Promise<Array<{ id: string; createdAt: number; report: EmployeeReport }>>;
|
||||||
saveReportVersion: (employeeId: string, report: Report) => Promise<void>;
|
saveReportVersion: (employeeId: string, report: EmployeeReport) => Promise<void>;
|
||||||
acceptInvite: (code: string) => Promise<void>;
|
acceptInvite: (code: string) => Promise<void>;
|
||||||
saveCompanyReport: (summary: string) => Promise<void>;
|
saveCompanyReport: (summary: string) => Promise<void>;
|
||||||
getCompanyReportHistory: () => Promise<Array<{ id: string; createdAt: number; summary: string }>>;
|
getCompanyReportHistory: () => Promise<Array<{ id: string; createdAt: number; summary: string }>>;
|
||||||
@@ -61,9 +42,9 @@ interface OrgContextType {
|
|||||||
seedInitialData: () => Promise<void>;
|
seedInitialData: () => Promise<void>;
|
||||||
isOwner: (employeeId?: string) => boolean;
|
isOwner: (employeeId?: string) => boolean;
|
||||||
submitEmployeeAnswers: (employeeId: string, answers: Record<string, string>) => Promise<any>;
|
submitEmployeeAnswers: (employeeId: string, answers: Record<string, string>) => Promise<any>;
|
||||||
generateEmployeeReport: (employee: Employee) => Promise<Report | null>;
|
generateEmployeeReport: (employee: Employee) => Promise<EmployeeReport | null>;
|
||||||
getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: Report; error?: string }>;
|
getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: EmployeeReport; error?: string }>;
|
||||||
getEmployeeReports: () => Promise<{ success: boolean; reports?: Report[]; error?: string }>;
|
getEmployeeReports: () => Promise<{ success: boolean; reports?: EmployeeReport[]; error?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
||||||
@@ -73,8 +54,8 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
const [org, setOrg] = useState<OrgData | null>(null);
|
const [org, setOrg] = useState<OrgData | null>(null);
|
||||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||||
const [submissions, setSubmissions] = useState<Record<string, Submission>>({});
|
const [submissions, setSubmissions] = useState<Record<string, Submission>>({});
|
||||||
const [reports, setReports] = useState<Record<string, Report>>({});
|
const [reports, setReports] = useState<Record<string, EmployeeReport>>({});
|
||||||
const [reportVersions, setReportVersions] = useState<Record<string, Array<{ id: string; createdAt: number; report: Report }>>>({});
|
const [reportVersions, setReportVersions] = useState<Record<string, Array<{ id: string; createdAt: number; report: EmployeeReport }>>>({});
|
||||||
const [companyReports, setCompanyReports] = useState<Array<{ id: string; createdAt: number; summary: string }>>([]);
|
const [companyReports, setCompanyReports] = useState<Array<{ id: string; createdAt: number; summary: string }>>([]);
|
||||||
const [fullCompanyReports, setFullCompanyReports] = useState<CompanyReport[]>([]);
|
const [fullCompanyReports, setFullCompanyReports] = useState<CompanyReport[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -97,19 +78,19 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
// Load organization data
|
// Load organization data
|
||||||
try {
|
try {
|
||||||
const orgData = await secureApi.getOrgData(orgId, user.uid);
|
const orgData = await secureApi.getOrgData();
|
||||||
setOrg({ orgId, ...orgData });
|
setOrg({ orgId, ...orgData });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not load org data, creating default:', error);
|
console.warn('Could not load org data, creating default:', error);
|
||||||
// Create default org if not found
|
// Create default org if not found
|
||||||
const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
|
const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
|
||||||
await secureApi.updateOrgData(orgId, user.uid, defaultOrg);
|
await secureApi.updateOrgData(defaultOrg);
|
||||||
setOrg({ orgId, ...defaultOrg });
|
setOrg({ orgId, ...defaultOrg });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load employees
|
// Load employees
|
||||||
try {
|
try {
|
||||||
const employeesData = await secureApi.getEmployees(orgId, user.uid);
|
const employeesData = await secureApi.getEmployees();
|
||||||
setEmployees(employeesData.map(emp => ({
|
setEmployees(employeesData.map(emp => ({
|
||||||
...emp,
|
...emp,
|
||||||
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
|
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
|
||||||
@@ -121,7 +102,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
// Load submissions
|
// Load submissions
|
||||||
try {
|
try {
|
||||||
const submissionsData = await secureApi.getSubmissions(orgId, user.uid);
|
const submissionsData = await secureApi.getSubmissions();
|
||||||
setSubmissions(submissionsData);
|
setSubmissions(submissionsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not load submissions:', error);
|
console.warn('Could not load submissions:', error);
|
||||||
@@ -130,8 +111,8 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
// Load reports
|
// Load reports
|
||||||
try {
|
try {
|
||||||
const reportsData = await secureApi.getReports(orgId, user.uid);
|
const reportsData = await secureApi.getReports();
|
||||||
setReports(reportsData as Record<string, Report>);
|
setReports(reportsData as Record<string, EmployeeReport>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not load reports:', error);
|
console.warn('Could not load reports:', error);
|
||||||
setReports({});
|
setReports({});
|
||||||
@@ -139,7 +120,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
// Load company reports
|
// Load company reports
|
||||||
try {
|
try {
|
||||||
const companyReportsData = await secureApi.getCompanyReports(orgId, user.uid);
|
const companyReportsData = await secureApi.getCompanyReports();
|
||||||
setFullCompanyReports(companyReportsData);
|
setFullCompanyReports(companyReportsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not load company reports:', error);
|
console.warn('Could not load company reports:', error);
|
||||||
@@ -162,7 +143,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureApi.updateOrgData(orgId, user.uid, data);
|
await secureApi.updateOrgData(data);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
|
const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
|
||||||
@@ -185,13 +166,13 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveReport = async (employeeId: string, report: Report) => {
|
const saveReport = async (employeeId: string, report: EmployeeReport) => {
|
||||||
if (!user?.uid) {
|
if (!user?.uid) {
|
||||||
throw new Error('User authentication required');
|
throw new Error('User authentication required');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedReport = await secureApi.saveReport(orgId, user.uid, employeeId, report);
|
const savedReport = await secureApi.saveReport(employeeId, report);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
setReports(prev => ({ ...prev, [employeeId]: savedReport }));
|
setReports(prev => ({ ...prev, [employeeId]: savedReport }));
|
||||||
@@ -206,7 +187,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use secure API for invites
|
// Use secure API for invites
|
||||||
const data = await secureApi.createInvitation(orgId, name, email);
|
const data = await secureApi.createInvitation(name, email);
|
||||||
|
|
||||||
console.log('Invite created successfully:', { code: data.code, employee: data.employee.name, inviteLink: data.inviteLink });
|
console.log('Invite created successfully:', { code: data.code, employee: data.employee.name, inviteLink: data.inviteLink });
|
||||||
|
|
||||||
@@ -241,7 +222,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveReportVersion = async (employeeId: string, report: Report) => {
|
const saveReportVersion = async (employeeId: string, report: EmployeeReport) => {
|
||||||
// This feature is not yet implemented in secure API
|
// This feature is not yet implemented in secure API
|
||||||
console.warn('Report versions not yet supported in secure API');
|
console.warn('Report versions not yet supported in secure API');
|
||||||
const version = { id: Date.now().toString(), createdAt: Date.now(), report };
|
const version = { id: Date.now().toString(), createdAt: Date.now(), report };
|
||||||
@@ -271,7 +252,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
summary
|
summary
|
||||||
};
|
};
|
||||||
|
|
||||||
await secureApi.saveCompanyReport(orgId, report);
|
await secureApi.saveCompanyReport(report);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
setCompanyReports(prev => [{ id: report.id, createdAt: report.createdAt, summary }, ...prev]);
|
setCompanyReports(prev => [{ id: report.id, createdAt: report.createdAt, summary }, ...prev]);
|
||||||
@@ -287,11 +268,11 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reports = await secureApi.getCompanyReports(orgId, user.uid);
|
const reports = await secureApi.getCompanyReports();
|
||||||
return reports.map(report => ({
|
return reports.map(report => ({
|
||||||
id: report.id,
|
id: report.id,
|
||||||
createdAt: report.createdAt || 0,
|
createdAt: report.createdAt || 0,
|
||||||
summary: report.summary || ''
|
summary: report.executiveSummary || ''
|
||||||
})).sort((a, b) => b.createdAt - a.createdAt);
|
})).sort((a, b) => b.createdAt - a.createdAt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get company report history:', error);
|
console.error('Failed to get company report history:', error);
|
||||||
@@ -318,7 +299,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureApi.saveCompanyReport(orgId, report);
|
await secureApi.saveCompanyReport(report);
|
||||||
|
|
||||||
// Update local state after successful save
|
// Update local state after successful save
|
||||||
setFullCompanyReports(prev => [report, ...prev]);
|
setFullCompanyReports(prev => [report, ...prev]);
|
||||||
@@ -334,7 +315,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reports = await secureApi.getCompanyReports(orgId, user.uid);
|
const reports = await secureApi.getCompanyReports();
|
||||||
return reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
return reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get full company report history:', error);
|
console.error('Failed to get full company report history:', error);
|
||||||
@@ -477,7 +458,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use secure API for all employee report operations
|
// Use secure API for all employee report operations
|
||||||
const report = await secureApi.getReports(orgId, user.uid);
|
const report = await secureApi.getReports();
|
||||||
const employeeReport = report[employeeId];
|
const employeeReport = report[employeeId];
|
||||||
|
|
||||||
if (employeeReport) {
|
if (employeeReport) {
|
||||||
@@ -497,7 +478,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use secure API for all employee report operations
|
// Use secure API for all employee report operations
|
||||||
const reportsData = await secureApi.getReports(orgId, user.uid);
|
const reportsData = await secureApi.getReports();
|
||||||
const reports = Object.values(reportsData);
|
const reports = Object.values(reportsData);
|
||||||
return { success: true, reports };
|
return { success: true, reports };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -533,7 +514,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
throw new Error('User authentication required');
|
throw new Error('User authentication required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await secureApi.createInvitation(orgId, name, email, role, department);
|
const data = await secureApi.createInvitation(name, email, role, department);
|
||||||
|
|
||||||
// Optimistically add employee shell (not yet active until consume)
|
// Optimistically add employee shell (not yet active until consume)
|
||||||
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
|
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
|
||||||
@@ -580,7 +561,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use secure API for submission
|
// Use secure API for submission
|
||||||
await secureApi.submitEmployeeAnswers(orgId, employeeId, answers);
|
await secureApi.submitEmployeeAnswers(employeeId, answers);
|
||||||
|
|
||||||
// Update local state for immediate UI feedback
|
// Update local state for immediate UI feedback
|
||||||
const convertedSubmission: Submission = {
|
const convertedSubmission: Submission = {
|
||||||
@@ -647,7 +628,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
if ((data as any).report) {
|
if ((data as any).report) {
|
||||||
console.log('Employee report generated successfully');
|
console.log('Employee report generated successfully');
|
||||||
const report = (data as any).report as Report;
|
const report = (data as any).report as EmployeeReport;
|
||||||
setReports(prev => ({ ...prev, [employee.id]: report }));
|
setReports(prev => ({ ...prev, [employee.id]: report }));
|
||||||
return report;
|
return report;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { isFirebaseConfigured } from '../services/firebase';
|
import { isFirebaseConfigured } from '../services/firebase';
|
||||||
import { API_URL } from '../constants';
|
import { API_URL } from '../constants';
|
||||||
import { demoStorage } from '../services/demoStorage';
|
import { secureApi } from '../services/secureApi';
|
||||||
|
|
||||||
interface UserOrganization {
|
interface UserOrganization {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -20,8 +20,8 @@ interface UserOrganizationsContextType {
|
|||||||
createOrganization: (name: string) => Promise<{ orgId: string; requiresSubscription?: boolean }>;
|
createOrganization: (name: string) => Promise<{ orgId: string; requiresSubscription?: boolean }>;
|
||||||
joinOrganization: (inviteCode: string) => Promise<string>;
|
joinOrganization: (inviteCode: string) => Promise<string>;
|
||||||
refreshOrganizations: () => Promise<void>;
|
refreshOrganizations: () => Promise<void>;
|
||||||
createCheckoutSession: (orgId: string, userEmail: string) => Promise<{ sessionUrl: string; sessionId: string }>;
|
createCheckoutSession: (userEmail: string) => Promise<{ sessionUrl: string; sessionId: string }>;
|
||||||
getSubscriptionStatus: (orgId: string) => Promise<any>;
|
getSubscriptionStatus: () => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserOrganizationsContext = createContext<UserOrganizationsContextType | undefined>(undefined);
|
const UserOrganizationsContext = createContext<UserOrganizationsContextType | undefined>(undefined);
|
||||||
@@ -41,15 +41,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Firebase mode - fetch from Cloud Functions
|
// Use secure API for organizations
|
||||||
const response = await fetch(`${API_URL}/getUserOrganizations?userId=${user.uid}`);
|
const data = await secureApi.getUserOrganizations();
|
||||||
if (response.ok) {
|
setOrganizations(data.organizations || []);
|
||||||
const data = await response.json();
|
|
||||||
setOrganizations(data.organizations || []);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load organizations:', response.status);
|
|
||||||
setOrganizations([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load organizations:', error);
|
console.error('Failed to load organizations:', error);
|
||||||
setOrganizations([]);
|
setOrganizations([]);
|
||||||
@@ -122,18 +116,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
let newOrg: UserOrganization;
|
let newOrg: UserOrganization;
|
||||||
let requiresSubscription = false;
|
let requiresSubscription = false;
|
||||||
|
|
||||||
// Firebase mode - use Cloud Function
|
// Use secure API for organization creation
|
||||||
const response = await fetch(`${API_URL}/createOrganization`, {
|
const data = await secureApi.createOrganization(name);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, userId: user.uid })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to create organization: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
newOrg = {
|
newOrg = {
|
||||||
orgId: data.orgId,
|
orgId: data.orgId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -197,18 +182,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
// return orgId;
|
// return orgId;
|
||||||
// } else {
|
// } else {
|
||||||
// Firebase mode - use Cloud Function
|
// Firebase mode - use Cloud Function
|
||||||
const response = await fetch(`${API_URL}/joinOrganization`, {
|
// Use secure API for joining organization
|
||||||
method: 'POST',
|
const data = await secureApi.joinOrganization(inviteCode);
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ userId: user.uid, inviteCode })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to join organization');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const userOrg: UserOrganization = {
|
const userOrg: UserOrganization = {
|
||||||
orgId: data.orgId,
|
orgId: data.orgId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -231,26 +207,11 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
await loadOrganizations();
|
await loadOrganizations();
|
||||||
};
|
};
|
||||||
|
|
||||||
const createCheckoutSession = async (orgId: string, userEmail: string): Promise<{ sessionUrl: string; sessionId: string }> => {
|
const createCheckoutSession = async (userEmail: string): Promise<{ sessionUrl: string; sessionId: string }> => {
|
||||||
if (!user) throw new Error('User not authenticated');
|
if (!user) throw new Error('User not authenticated');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/createCheckoutSession`, {
|
const data = await secureApi.createCheckoutSession(userEmail);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
orgId,
|
|
||||||
userId: user.uid,
|
|
||||||
userEmail
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to create checkout session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return {
|
return {
|
||||||
sessionUrl: data.sessionUrl,
|
sessionUrl: data.sessionUrl,
|
||||||
sessionId: data.sessionId
|
sessionId: data.sessionId
|
||||||
@@ -261,15 +222,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSubscriptionStatus = async (orgId: string) => {
|
const getSubscriptionStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/getSubscriptionStatus?orgId=${orgId}`);
|
const data = await secureApi.getSubscriptionStatus();
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to get subscription status');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get subscription status:', error);
|
console.error('Failed to get subscription status:', error);
|
||||||
|
|||||||
@@ -19,13 +19,18 @@ export interface OnboardingStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface OnboardingData {
|
export interface OnboardingData {
|
||||||
|
companyName: string;
|
||||||
|
yourName: string;
|
||||||
|
companyLogo: string;
|
||||||
[key: string]: string | string[];
|
[key: string]: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeOnboardingData = (): OnboardingData => {
|
export const initializeOnboardingData = (): OnboardingData => {
|
||||||
const data: OnboardingData = {
|
const data: OnboardingData = {
|
||||||
// Ensure required form fields are initialized
|
// Ensure required form fields are initialized
|
||||||
companyDetails: '',
|
companyName: '',
|
||||||
|
yourName: '',
|
||||||
|
companyLogo: '',
|
||||||
};
|
};
|
||||||
onboardingSteps.forEach(step => {
|
onboardingSteps.forEach(step => {
|
||||||
if (step.field) {
|
if (step.field) {
|
||||||
|
|||||||
@@ -1,526 +0,0 @@
|
|||||||
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';
|
|
||||||
import ReportDetail from './ReportDetail';
|
|
||||||
|
|
||||||
interface EmployeeReportProps {
|
|
||||||
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.reasoning}</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.quarterlyGoals.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">Risk Mitigation</h4>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{report.forwardOperatingPlan.riskMitigation.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;
|
|
||||||
onViewReport?: (report: Report, employeeName: string) => void;
|
|
||||||
}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport, onViewReport }) => {
|
|
||||||
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={() => onViewReport?.(report, employee.name)}
|
|
||||||
>
|
|
||||||
View Full Report
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
>
|
|
||||||
{isExpanded ? 'Hide' : 'View'} Summary
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isOwner && mode === 'reports' && (
|
|
||||||
<Button
|
|
||||||
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 EmployeeReport: React.FC<EmployeeReportProps> = ({ mode }) => {
|
|
||||||
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, saveReport, orgId } = useOrg();
|
|
||||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
|
||||||
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
|
|
||||||
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
|
||||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | Report; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Load company report for owners
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Use the OrgContext method instead of direct API call
|
|
||||||
const report = await generateEmployeeReport(employee);
|
|
||||||
|
|
||||||
if (report) {
|
|
||||||
console.log('Report generated and saved successfully for:', employee.name);
|
|
||||||
} else {
|
|
||||||
console.error('Report generation failed for:', employee.name);
|
|
||||||
// Show user-friendly error
|
|
||||||
alert(`Failed to generate report for ${employee.name}. Please try again.`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating report:', error);
|
|
||||||
alert(`Error generating report for ${employee.name}: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
setGeneratingReports(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(employee.id);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerateCompanyReport = async () => {
|
|
||||||
setGeneratingCompanyReport(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Generating company report for org:', orgId);
|
|
||||||
const newReport = await generateCompanyReport();
|
|
||||||
console.log('Received new company report:', newReport);
|
|
||||||
setCompanyReport(newReport);
|
|
||||||
console.log('Company report generated and state updated successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating company report:', error);
|
|
||||||
alert(`Error generating company report: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
setGeneratingCompanyReport(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentUserIsOwner = isOwner(user?.uid || '');
|
|
||||||
|
|
||||||
// Filter employees based on user access
|
|
||||||
const visibleEmployees = currentUserIsOwner
|
|
||||||
? 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' && (
|
|
||||||
<div className="mb-6">
|
|
||||||
{companyReport ? (
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary]">Company Report</h2>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setSelectedReport({ report: companyReport, type: 'company' })}
|
|
||||||
>
|
|
||||||
View Full Report
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleGenerateCompanyReport}
|
|
||||||
disabled={generatingCompanyReport}
|
|
||||||
>
|
|
||||||
{generatingCompanyReport ? 'Regenerating...' : 'Regenerate Report'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CompanyReportCard report={companyReport} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-2">
|
|
||||||
Generate Company Report
|
|
||||||
</h3>
|
|
||||||
<p className="text-[--text-secondary] mb-4">
|
|
||||||
Create a comprehensive AI-powered report analyzing your organization's performance,
|
|
||||||
strengths, and recommendations based on employee data.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={handleGenerateCompanyReport}
|
|
||||||
disabled={generatingCompanyReport}
|
|
||||||
>
|
|
||||||
{generatingCompanyReport ? 'Generating Report...' : 'Generate Company Report'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Employee Cards */}
|
|
||||||
<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)}
|
|
||||||
onViewReport={(report, employeeName) => setSelectedReport({ report, type: 'employee', employeeName })}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Report Detail Modal */}
|
|
||||||
{selectedReport && (
|
|
||||||
<ReportDetail
|
|
||||||
report={selectedReport.report}
|
|
||||||
type={selectedReport.type}
|
|
||||||
employeeName={selectedReport.employeeName}
|
|
||||||
onClose={() => setSelectedReport(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeReport;
|
|
||||||
@@ -66,17 +66,17 @@ const YesNoChoice: React.FC<{
|
|||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||||
<div
|
<div
|
||||||
onClick={() => onChange('No')}
|
onClick={() => onChange('No')}
|
||||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No' ? 'bg-Neutrals-NeutralSlate800' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-Neutrals-NeutralSlate200'}`}
|
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate800]'}`}
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'}`}>
|
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'}`}>
|
||||||
No
|
No
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => onChange('Yes')}
|
onClick={() => onChange('Yes')}
|
||||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes' ? 'bg-Neutrals-NeutralSlate800' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-Neutrals-NeutralSlate200'}`}
|
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate800]'}`}
|
||||||
>
|
>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'}`}>
|
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'}`}>
|
||||||
Yes
|
Yes
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +217,7 @@ const EmployeeFormStep1: React.FC<{ onNext: (data: any) => void }> = ({ onNext }
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
@@ -234,7 +234,7 @@ const EmployeeFormStep1: React.FC<{ onNext: (data: any) => void }> = ({ onNext }
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What is your role at the company?</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What is your role at the company?</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
@@ -251,7 +251,7 @@ const EmployeeFormStep1: React.FC<{ onNext: (data: any) => void }> = ({ onNext }
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What department do you work in?</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What department do you work in?</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
@@ -300,7 +300,7 @@ const EmployeeFormStep2: React.FC<{ onNext: (data: any) => void; onBack: () => v
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
@@ -317,7 +317,7 @@ const EmployeeFormStep2: React.FC<{ onNext: (data: any) => void; onBack: () => v
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
@@ -334,7 +334,7 @@ const EmployeeFormStep2: React.FC<{ onNext: (data: any) => void; onBack: () => v
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What is the name of your Company and department?</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">What is the name of your Company and department?</div>
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
|
|||||||
@@ -264,13 +264,13 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
// Early return for invite flow loading state
|
// Early return for invite flow loading state
|
||||||
if (isInviteFlow && isLoadingInvite) {
|
if (isInviteFlow && isLoadingInvite) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-Neutrals-NeutralSlate0 py-8 px-4 flex items-center justify-center">
|
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<div className="w-16 h-16 bg-Brand-Orange rounded-full flex items-center justify-center font-bold text-Other-White text-2xl mx-auto mb-4">
|
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-Other-White text-2xl mx-auto mb-4">
|
||||||
A
|
A
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-Neutrals-NeutralSlate950 mb-4">Loading Your Invitation...</h1>
|
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Loading Your Invitation...</h1>
|
||||||
<p className="text-Neutrals-NeutralSlate500">Please wait while we verify your invitation.</p>
|
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we verify your invitation.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -279,16 +279,16 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
// Early return for invite flow error state
|
// Early return for invite flow error state
|
||||||
if (isInviteFlow && error && currentStep === 1) {
|
if (isInviteFlow && error && currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-Neutrals-NeutralSlate0 py-8 px-4 flex items-center justify-center">
|
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<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-Other-White text-2xl mx-auto mb-4">
|
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-Other-White text-2xl mx-auto mb-4">
|
||||||
!
|
!
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-Neutrals-NeutralSlate950 mb-4">Invitation Error</h1>
|
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Invitation Error</h1>
|
||||||
<p className="text-Neutrals-NeutralSlate500 mb-6">{error}</p>
|
<p className="text-[--Neutrals-NeutralSlate500] mb-6">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = '/'}
|
onClick={() => window.location.href = '/'}
|
||||||
className="px-6 py-3 bg-Brand-Orange text-Other-White rounded-lg hover:bg-orange-600"
|
className="px-6 py-3 bg-[--Brand-Orange] text-Other-White rounded-lg hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
Return to Homepage
|
Return to Homepage
|
||||||
</button>
|
</button>
|
||||||
@@ -680,13 +680,13 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
|
|
||||||
if (isSubmitting) {
|
if (isSubmitting) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-Neutrals-NeutralSlate0 py-8 px-4 flex items-center justify-center">
|
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<div className="w-16 h-16 bg-Brand-Orange rounded-full flex items-center justify-center font-bold text-Other-White text-2xl mx-auto mb-4 animate-pulse">
|
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-Other-White text-2xl mx-auto mb-4 animate-pulse">
|
||||||
A
|
A
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-Neutrals-NeutralSlate950 mb-4">Submitting Your Responses...</h1>
|
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Submitting Your Responses...</h1>
|
||||||
<p className="text-Neutrals-NeutralSlate500">Please wait while we process your assessment and generate your report.</p>
|
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we process your assessment and generate your report.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { onboardingSteps, OnboardingData, initializeOnboardingData } from '../data/onboardingSteps';
|
import { onboardingSteps, OnboardingData, initializeOnboardingData } from '../data/onboardingSteps';
|
||||||
import { secureApiPOST } from '../services/secureApi';
|
import { secureApiPOST, secureApi } from '../services/secureApi';
|
||||||
import {
|
import {
|
||||||
FigmaOnboardingIntro,
|
FigmaOnboardingIntro,
|
||||||
FigmaOnboardingQuestion,
|
FigmaOnboardingQuestion,
|
||||||
FigmaOnboardingMultipleChoice,
|
FigmaOnboardingMultipleChoice,
|
||||||
FigmaOnboardingForm
|
FigmaOnboardingForm
|
||||||
} from '../components/onboarding/FigmaOnboardingComponents';
|
} from '../components/onboarding/FigmaOnboardingComponents';
|
||||||
|
|
||||||
const Onboarding: React.FC = () => {
|
const Onboarding: React.FC = () => {
|
||||||
const { org, upsertOrg, generateCompanyWiki } = useOrg();
|
const { org, upsertOrg, generateCompanyWiki } = useOrg();
|
||||||
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,9 +38,11 @@ const Onboarding: React.FC = () => {
|
|||||||
|
|
||||||
switch (currentStep.type) {
|
switch (currentStep.type) {
|
||||||
case 'form':
|
case 'form':
|
||||||
// Check required fields for form step - using companyDetails field
|
// Check required fields for form step - company name and user name
|
||||||
const companyDetails = formData.companyDetails;
|
const companyName = formData.companyName;
|
||||||
return typeof companyDetails === 'string' && companyDetails.trim().length > 0;
|
const yourName = formData.yourName;
|
||||||
|
return typeof companyName === 'string' && companyName.trim().length > 0 &&
|
||||||
|
typeof yourName === 'string' && yourName.trim().length > 0;
|
||||||
|
|
||||||
case 'question':
|
case 'question':
|
||||||
// Check if field is filled
|
// Check if field is filled
|
||||||
@@ -75,25 +79,15 @@ const Onboarding: React.FC = () => {
|
|||||||
// Final step: submit all data and complete onboarding
|
// Final step: submit all data and complete onboarding
|
||||||
setIsGeneratingReport(true);
|
setIsGeneratingReport(true);
|
||||||
try {
|
try {
|
||||||
// Submit the complete onboarding data
|
await upsertOrg({
|
||||||
const response = await secureApiPOST('onboarding/complete', formData);
|
...org,
|
||||||
|
companyName: formData.companyName,
|
||||||
if (response.success) {
|
companyLogo: formData.companyLogo,
|
||||||
// Update org with completion status
|
onboardingData: formData,
|
||||||
const updatedOrg = {
|
onboardingCompleted: true,
|
||||||
...org,
|
updatedAt: Date.now(),
|
||||||
name: formData.companyName,
|
});
|
||||||
onboardingCompleted: true,
|
navigate('/reports', { replace: true });
|
||||||
updatedAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
await upsertOrg(updatedOrg);
|
|
||||||
|
|
||||||
// Navigate to reports
|
|
||||||
navigate('/reports', { replace: true });
|
|
||||||
} else {
|
|
||||||
throw new Error(response.error || 'Failed to complete onboarding');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error completing onboarding:', error);
|
console.error('Error completing onboarding:', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
@@ -160,18 +154,18 @@ const Onboarding: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'question':
|
case 'question':
|
||||||
const questionValue = currentStep.fieldMapping
|
const questionValue = currentStep.field
|
||||||
? String(formData[currentStep.fieldMapping as keyof OnboardingData] || '')
|
? String(formData[currentStep.field as keyof OnboardingData] || '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FigmaOnboardingQuestion
|
<FigmaOnboardingQuestion
|
||||||
question={currentStep.question || ''}
|
question={currentStep.title || ''}
|
||||||
placeholder={currentStep.placeholder}
|
placeholder={currentStep.placeholder}
|
||||||
value={questionValue}
|
value={questionValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (currentStep.fieldMapping) {
|
if (currentStep.field) {
|
||||||
updateFormData(currentStep.fieldMapping as keyof OnboardingData, value);
|
updateFormData(currentStep.field as keyof OnboardingData, value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
@@ -180,24 +174,24 @@ const Onboarding: React.FC = () => {
|
|||||||
totalInSection={sectionInfo.totalInSection}
|
totalInSection={sectionInfo.totalInSection}
|
||||||
sectionName={sectionInfo.sectionName}
|
sectionName={sectionInfo.sectionName}
|
||||||
canProceed={canProceed()}
|
canProceed={canProceed()}
|
||||||
canSkip={currentStep.optional}
|
canSkip={!currentStep.required}
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'multiple_choice':
|
case 'multiple_choice':
|
||||||
const multipleChoiceValue = currentStep.fieldMapping
|
const multipleChoiceValue = currentStep.field
|
||||||
? String(formData[currentStep.fieldMapping as keyof OnboardingData] || '')
|
? String(formData[currentStep.field as keyof OnboardingData] || '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FigmaOnboardingMultipleChoice
|
<FigmaOnboardingMultipleChoice
|
||||||
question={currentStep.question || ''}
|
question={currentStep.title || ''}
|
||||||
options={currentStep.options || []}
|
options={currentStep.options || []}
|
||||||
selectedValue={multipleChoiceValue}
|
selectedValue={multipleChoiceValue}
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
if (currentStep.fieldMapping) {
|
if (currentStep.field) {
|
||||||
updateFormData(currentStep.fieldMapping as keyof OnboardingData, value);
|
updateFormData(currentStep.field as keyof OnboardingData, value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
@@ -205,7 +199,7 @@ const Onboarding: React.FC = () => {
|
|||||||
sectionPosition={sectionInfo.sectionPosition}
|
sectionPosition={sectionInfo.sectionPosition}
|
||||||
totalInSection={sectionInfo.totalInSection}
|
totalInSection={sectionInfo.totalInSection}
|
||||||
sectionName={sectionInfo.sectionName}
|
sectionName={sectionInfo.sectionName}
|
||||||
canSkip={currentStep.optional}
|
canSkip={!currentStep.required}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,411 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, Button } from '../components/UiKit';
|
|
||||||
import { CompanyReport, Report } from '../types';
|
|
||||||
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
|
||||||
import ScoreBarList from '../components/charts/ScoreBarList';
|
|
||||||
|
|
||||||
interface ReportDetailProps {
|
|
||||||
report: CompanyReport | Report;
|
|
||||||
type: 'company' | 'employee';
|
|
||||||
employeeName?: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName, onClose }) => {
|
|
||||||
if (type === 'company') {
|
|
||||||
const companyReport = report as CompanyReport;
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-[--text-primary]">Company Report</h1>
|
|
||||||
<p className="text-[--text-secondary]">Last updated: {new Date(companyReport.createdAt).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm">Download as PDF</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Executive Summary */}
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Executive Summary</h2>
|
|
||||||
<p className="text-[--text-secondary] whitespace-pre-line leading-relaxed">
|
|
||||||
{companyReport.executiveSummary}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Overview Stats */}
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Company Overview</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
|
|
||||||
<div className="text-3xl font-bold text-blue-500 mb-1">{companyReport.overview.totalEmployees}</div>
|
|
||||||
<div className="text-sm text-[--text-secondary]">Total Employees</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
|
|
||||||
<div className="text-3xl font-bold text-green-500 mb-1">{companyReport.overview.departmentBreakdown?.length || 0}</div>
|
|
||||||
<div className="text-sm text-[--text-secondary]">Departments</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
|
|
||||||
<div className="text-3xl font-bold text-purple-500 mb-1">{companyReport.overview.averagePerformanceScore || 'N/A'}</div>
|
|
||||||
<div className="text-sm text-[--text-secondary]">Avg Performance</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
|
|
||||||
<div className="text-3xl font-bold text-orange-500 mb-1">{companyReport.overview.riskLevel || 'Low'}</div>
|
|
||||||
<div className="text-sm text-[--text-secondary]">Risk Level</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Key Personnel Changes */}
|
|
||||||
{companyReport.keyPersonnelChanges && companyReport.keyPersonnelChanges.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
|
|
||||||
Key Personnel Changes
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{companyReport.keyPersonnelChanges.map((change, idx) => (
|
|
||||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-[--text-primary]">{change.employeeName}</h3>
|
|
||||||
<p className="text-sm text-[--text-secondary]">{change.role} • {change.department}</p>
|
|
||||||
</div>
|
|
||||||
<span className={`px-3 py-1 text-xs rounded-full font-medium ${change.changeType === 'departure' ? 'bg-red-100 text-red-800' :
|
|
||||||
change.changeType === 'promotion' ? 'bg-green-100 text-green-800' :
|
|
||||||
'bg-blue-100 text-blue-800'
|
|
||||||
}`}>
|
|
||||||
{change.changeType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[--text-secondary] text-sm">{change.impact}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Immediate Hiring Needs */}
|
|
||||||
{companyReport.immediateHiringNeeds && companyReport.immediateHiringNeeds.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-red-500 rounded-full mr-3"></span>
|
|
||||||
Immediate Hiring Needs
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{companyReport.immediateHiringNeeds.map((need, idx) => (
|
|
||||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="font-semibold text-[--text-primary]">{need.role}</h3>
|
|
||||||
<span className={`px-2 py-1 text-xs rounded-full font-medium ${need.urgency === 'high' ? 'bg-red-100 text-red-800' :
|
|
||||||
need.urgency === 'medium' ? 'bg-yellow-100 text-yellow-800' :
|
|
||||||
'bg-green-100 text-green-800'
|
|
||||||
}`}>
|
|
||||||
{need.urgency}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
|
|
||||||
<p className="text-sm text-[--text-secondary]">{need.reasoning}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Forward Operating Plan */}
|
|
||||||
{companyReport.forwardOperatingPlan && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
|
||||||
Forward Operating Plan
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-3">Next Quarter Goals</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{companyReport.forwardOperatingPlan.quarterlyGoals?.map((goal, idx) => (
|
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
|
||||||
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
|
||||||
{goal}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-3">Key Initiatives</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{companyReport.forwardOperatingPlan.riskMitigation?.map((initiative, idx) => (
|
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
|
||||||
{initiative}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Organizational Strengths */}
|
|
||||||
{companyReport.organizationalStrengths && companyReport.organizationalStrengths.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
|
|
||||||
Organizational Strengths
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{companyReport.organizationalStrengths.map((strength, idx) => (
|
|
||||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<span className="text-3xl">{strength.icon || '💪'}</span>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-1">{strength.area || strength.description}</h3>
|
|
||||||
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Grading Overview */}
|
|
||||||
{companyReport.gradingOverview && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-indigo-500 rounded-full mr-3"></span>
|
|
||||||
Grading Overview
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
||||||
{Object.entries(companyReport.gradingOverview).map(([category, score], idx) => (
|
|
||||||
<div key={idx} className="text-center p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<div className="text-3xl font-bold text-[--text-primary] mb-2">{score}/5</div>
|
|
||||||
<div className="text-sm text-[--text-secondary] capitalize">
|
|
||||||
{category.replace(/([A-Z])/g, ' $1').trim()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Organizational Impact Summary */}
|
|
||||||
{companyReport.organizationalImpactSummary && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
|
|
||||||
Organizational Impact Summary
|
|
||||||
</h2>
|
|
||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<p className="text-[--text-secondary] leading-relaxed">
|
|
||||||
{companyReport.organizationalImpactSummary}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const employeeReport = report as Report;
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-[--text-primary]">{employeeName}'s Performance Report</h1>
|
|
||||||
<p className="text-[--text-secondary]">{employeeReport.role} • {employeeReport.department}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm">Download as PDF</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Self-Reported Role & Output */}
|
|
||||||
{employeeReport.roleAndOutput && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Self-Reported Role & Output</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-2">Responsibilities</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm leading-relaxed">{employeeReport.roleAndOutput.responsibilities}</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-1">Clarity on Role</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.clarityOnRole}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-1">Self-Rated Output</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.selfRatedOutput}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Performance Charts */}
|
|
||||||
{employeeReport.grading?.[0]?.scores && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Performance Analysis</h2>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-[--background-tertiary] rounded-lg p-6">
|
|
||||||
<RadarPerformanceChart
|
|
||||||
title="Performance Profile"
|
|
||||||
data={employeeReport.grading[0].scores.map(s => ({
|
|
||||||
label: s.subject,
|
|
||||||
value: (s.value / s.fullMark) * 100
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="bg-[--background-tertiary] rounded-lg p-6">
|
|
||||||
<ScoreBarList
|
|
||||||
title="Score Breakdown"
|
|
||||||
items={employeeReport.grading[0].scores.map(s => ({
|
|
||||||
label: s.subject,
|
|
||||||
value: s.value,
|
|
||||||
max: s.fullMark
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Behavioral & Psychological Insights */}
|
|
||||||
{employeeReport.insights && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Behavioral & Psychological Insights</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-3">Personality Traits</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm leading-relaxed mb-4">
|
|
||||||
{employeeReport.insights.personalityTraits || 'No personality traits data available.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-3">Self-awareness</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm leading-relaxed">
|
|
||||||
{employeeReport.insights.selfAwareness || 'No self-awareness data available.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-3">Psychological Indicators</h3>
|
|
||||||
<ul className="space-y-2 mb-4">
|
|
||||||
{employeeReport.insights.psychologicalIndicators?.map((indicator, idx) => (
|
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
|
||||||
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
|
||||||
{indicator}
|
|
||||||
</li>
|
|
||||||
)) || <li className="text-sm text-[--text-secondary]">No psychological indicators available.</li>}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-3">Growth Desire</h3>
|
|
||||||
<p className="text-[--text-secondary] text-sm leading-relaxed">
|
|
||||||
{employeeReport.insights.growthDesire || 'No growth desire data available.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Strengths & Weaknesses */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
|
|
||||||
Strengths
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{employeeReport.insights?.strengths?.map((strength, idx) => (
|
|
||||||
<div key={idx} className="flex items-center space-x-2">
|
|
||||||
<span className="text-green-500">✓</span>
|
|
||||||
<span className="text-sm text-[--text-secondary]">{strength}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
|
|
||||||
Development Areas
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{employeeReport.insights?.weaknesses?.map((weakness, idx) => (
|
|
||||||
<div key={idx} className="flex items-center space-x-2">
|
|
||||||
<span className="text-orange-500">!</span>
|
|
||||||
<span className="text-sm text-[--text-secondary]">{weakness}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Opportunities */}
|
|
||||||
{employeeReport.opportunities && employeeReport.opportunities?.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
|
||||||
Opportunities
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{employeeReport.opportunities.map((opp, idx) => (
|
|
||||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<h3 className="font-medium text-[--text-primary] mb-2">{opp.roleAdjustment || 'Opportunity'}</h3>
|
|
||||||
<p className="text-sm text-[--text-secondary]">{opp.accountabilitySupport || opp.description }</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Risks */}
|
|
||||||
{employeeReport.risks && employeeReport.risks.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-3"></span>
|
|
||||||
Risks
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{employeeReport.risks.map((risk, idx) => (
|
|
||||||
<div key={idx} className="flex items-start space-x-2 p-3 bg-yellow-50 rounded-lg">
|
|
||||||
<span className="text-yellow-500 mt-0.5">⚠</span>
|
|
||||||
<span className="text-sm text-gray-700">{risk}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Recommendations */}
|
|
||||||
{employeeReport.recommendations && employeeReport.recommendations.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
|
|
||||||
Recommendations
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{employeeReport.recommendations.map((rec, idx) => (
|
|
||||||
<div key={idx} className="flex items-start space-x-3 p-3 bg-[--background-tertiary] rounded-lg">
|
|
||||||
<span className="w-2 h-2 bg-purple-500 rounded-full mt-2 flex-shrink-0"></span>
|
|
||||||
<span className="text-sm text-[--text-secondary]">{rec}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReportDetail;
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { CompanyReport, Employee, Report } from '../types';
|
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
||||||
import { SAMPLE_COMPANY_REPORT } from '../constants';
|
import { SAMPLE_COMPANY_REPORT } from '../constants';
|
||||||
|
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
||||||
|
|
||||||
const Reports: React.FC = () => {
|
const Reports: React.FC = () => {
|
||||||
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId } = useOrg();
|
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId } = useOrg();
|
||||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | Report; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
|
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
|
||||||
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
||||||
@@ -75,22 +76,22 @@ const Reports: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[4558px] p-4 bg-Neutrals-NeutralSlate200 inline-flex justify-start items-start overflow-hidden">
|
<div className="w-[1440px] h-[4558px] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-hidden">
|
||||||
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
|
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
|
||||||
|
|
||||||
{/* Left Sidebar - Navigation */}
|
{/* Left Sidebar - Navigation */}
|
||||||
<div className="w-64 self-stretch max-w-64 min-w-64 px-3 pt-4 pb-3 bg-Neutrals-NeutralSlate0 border-r border-Neutrals-NeutralSlate200 inline-flex flex-col justify-between items-center overflow-hidden">
|
<div className="w-64 self-stretch max-w-64 min-w-64 px-3 pt-4 pb-3 bg-[--Neutrals-NeutralSlate0] border-r border-Neutrals-NeutralSlate200 inline-flex flex-col justify-between items-center overflow-hidden">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
||||||
{/* Company Logo */}
|
{/* Company Logo */}
|
||||||
<div className="w-60 pl-2 pr-4 py-2 bg-Neutrals-NeutralSlate0 rounded-3xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-between items-center overflow-hidden">
|
<div className="w-60 pl-2 pr-4 py-2 bg-[--Neutrals-NeutralSlate0] rounded-3xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-between items-center overflow-hidden">
|
||||||
<div className="flex-1 flex justify-start items-center gap-2">
|
<div className="flex-1 flex justify-start items-center gap-2">
|
||||||
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
||||||
<div className="w-8 h-8 relative bg-Brand-Orange rounded-full outline outline-[1.60px] outline-offset-[-1.60px] outline-white/10 overflow-hidden">
|
<div className="w-8 h-8 relative bg-[--Brand-Orange] rounded-full outline outline-[1.60px] outline-offset-[-1.60px] outline-white/10 overflow-hidden">
|
||||||
<div className="w-8 h-8 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
<div className="w-8 h-8 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-base font-medium font-['Inter'] leading-normal">Auditly Company</div>
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">Auditly Company</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -109,7 +110,7 @@ const Reports: React.FC = () => {
|
|||||||
<path d="M7.5 17.4996V11.333C7.5 10.8662 7.5 10.6329 7.59083 10.4546C7.67072 10.2978 7.79821 10.1703 7.95501 10.0904C8.13327 9.99962 8.36662 9.99962 8.83333 9.99962H11.1667C11.6334 9.99962 11.8667 9.99962 12.045 10.0904C12.2018 10.1703 12.3293 10.2978 12.4092 10.4546C12.5 10.6329 12.5 10.8662 12.5 11.333V17.4996M9.18141 2.30297L3.52949 6.6989C3.15168 6.99275 2.96278 7.13968 2.82669 7.32368C2.70614 7.48667 2.61633 7.67029 2.56169 7.86551C2.5 8.0859 2.5 8.32521 2.5 8.80384V14.833C2.5 15.7664 2.5 16.2331 2.68166 16.5896C2.84144 16.9032 3.09641 17.1582 3.41002 17.318C3.76654 17.4996 4.23325 17.4996 5.16667 17.4996H14.8333C15.7668 17.4996 16.2335 17.4996 16.59 17.318C16.9036 17.1582 17.1586 16.9032 17.3183 16.5896C17.5 16.2331 17.5 15.7664 17.5 14.833V8.80384C17.5 8.32521 17.5 8.0859 17.4383 7.86551C17.3837 7.67029 17.2939 7.48667 17.1733 7.32368C17.0372 7.13968 16.8483 6.99275 16.4705 6.69891L10.8186 2.30297C10.5258 2.07526 10.3794 1.9614 10.2178 1.91763C10.0752 1.87902 9.92484 1.87902 9.78221 1.91763C9.62057 1.9614 9.47418 2.07526 9.18141 2.30297Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M7.5 17.4996V11.333C7.5 10.8662 7.5 10.6329 7.59083 10.4546C7.67072 10.2978 7.79821 10.1703 7.95501 10.0904C8.13327 9.99962 8.36662 9.99962 8.83333 9.99962H11.1667C11.6334 9.99962 11.8667 9.99962 12.045 10.0904C12.2018 10.1703 12.3293 10.2978 12.4092 10.4546C12.5 10.6329 12.5 10.8662 12.5 11.333V17.4996M9.18141 2.30297L3.52949 6.6989C3.15168 6.99275 2.96278 7.13968 2.82669 7.32368C2.70614 7.48667 2.61633 7.67029 2.56169 7.86551C2.5 8.0859 2.5 8.32521 2.5 8.80384V14.833C2.5 15.7664 2.5 16.2331 2.68166 16.5896C2.84144 16.9032 3.09641 17.1582 3.41002 17.318C3.76654 17.4996 4.23325 17.4996 5.16667 17.4996H14.8333C15.7668 17.4996 16.2335 17.4996 16.59 17.318C16.9036 17.1582 17.1586 16.9032 17.3183 16.5896C17.5 16.2331 17.5 15.7664 17.5 14.833V8.80384C17.5 8.32521 17.5 8.0859 17.4383 7.86551C17.3837 7.67029 17.2939 7.48667 17.1733 7.32368C17.0372 7.13968 16.8483 6.99275 16.4705 6.69891L10.8186 2.30297C10.5258 2.07526 10.3794 1.9614 10.2178 1.91763C10.0752 1.87902 9.92484 1.87902 9.78221 1.91763C9.62057 1.9614 9.47418 2.07526 9.18141 2.30297Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">Company Wiki</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Company Wiki</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -117,9 +118,9 @@ const Reports: React.FC = () => {
|
|||||||
<path d="M11.6667 9.16602H6.66667M8.33333 12.4993H6.66667M13.3333 5.83268H6.66667M16.6667 5.66602V14.3327C16.6667 15.7328 16.6667 16.4329 16.3942 16.9677C16.1545 17.4381 15.772 17.8205 15.3016 18.0602C14.7669 18.3327 14.0668 18.3327 12.6667 18.3327H7.33333C5.9332 18.3327 5.23314 18.3327 4.69836 18.0602C4.22795 17.8205 3.8455 17.4381 3.60582 16.9677C3.33333 16.4329 3.33333 15.7328 3.33333 14.3327V5.66602C3.33333 4.26588 3.33333 3.56582 3.60582 3.03104C3.8455 2.56063 4.22795 2.17818 4.69836 1.9385C5.23314 1.66602 5.9332 1.66602 7.33333 1.66602H12.6667C14.0668 1.66602 14.7669 1.66602 15.3016 1.9385C15.772 2.17818 16.1545 2.56063 16.3942 3.03104C16.6667 3.56582 16.6667 4.26588 16.6667 5.66602Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M11.6667 9.16602H6.66667M8.33333 12.4993H6.66667M13.3333 5.83268H6.66667M16.6667 5.66602V14.3327C16.6667 15.7328 16.6667 16.4329 16.3942 16.9677C16.1545 17.4381 15.772 17.8205 15.3016 18.0602C14.7669 18.3327 14.0668 18.3327 12.6667 18.3327H7.33333C5.9332 18.3327 5.23314 18.3327 4.69836 18.0602C4.22795 17.8205 3.8455 17.4381 3.60582 16.9677C3.33333 16.4329 3.33333 15.7328 3.33333 14.3327V5.66602C3.33333 4.26588 3.33333 3.56582 3.60582 3.03104C3.8455 2.56063 4.22795 2.17818 4.69836 1.9385C5.23314 1.66602 5.9332 1.66602 7.33333 1.66602H12.6667C14.0668 1.66602 14.7669 1.66602 15.3016 1.9385C15.772 2.17818 16.1545 2.56063 16.3942 3.03104C16.6667 3.56582 16.6667 4.26588 16.6667 5.66602Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">Submissions</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Submissions</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-60 px-4 py-2.5 bg-Neutrals-NeutralSlate100 rounded-[34px] inline-flex justify-start items-center gap-2">
|
<div className="w-60 px-4 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[34px] inline-flex justify-start items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clipPath="url(#clip0_1042_5096)">
|
<g clipPath="url(#clip0_1042_5096)">
|
||||||
@@ -132,7 +133,7 @@ const Reports: React.FC = () => {
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Reports</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reports</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -140,7 +141,7 @@ const Reports: React.FC = () => {
|
|||||||
<path d="M17.4997 9.58333C17.4997 13.4953 14.3284 16.6667 10.4164 16.6667C9.51904 16.6667 8.66069 16.4998 7.87065 16.1954C7.7262 16.1398 7.65398 16.112 7.59655 16.0987C7.54006 16.0857 7.49917 16.0803 7.44124 16.0781C7.38234 16.0758 7.31772 16.0825 7.18849 16.0958L2.92097 16.537C2.5141 16.579 2.31067 16.6001 2.19067 16.5269C2.08614 16.4631 2.01495 16.3566 1.996 16.2356C1.97425 16.0968 2.07146 15.9168 2.26588 15.557L3.62893 13.034C3.74118 12.8262 3.79731 12.7223 3.82273 12.6225C3.84784 12.5238 3.85391 12.4527 3.84588 12.3512C3.83775 12.2484 3.79266 12.1147 3.7025 11.8472C3.46289 11.1363 3.33302 10.375 3.33302 9.58333C3.33302 5.67132 6.50434 2.5 10.4164 2.5C14.3284 2.5 17.4997 5.67132 17.4997 9.58333Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M17.4997 9.58333C17.4997 13.4953 14.3284 16.6667 10.4164 16.6667C9.51904 16.6667 8.66069 16.4998 7.87065 16.1954C7.7262 16.1398 7.65398 16.112 7.59655 16.0987C7.54006 16.0857 7.49917 16.0803 7.44124 16.0781C7.38234 16.0758 7.31772 16.0825 7.18849 16.0958L2.92097 16.537C2.5141 16.579 2.31067 16.6001 2.19067 16.5269C2.08614 16.4631 2.01495 16.3566 1.996 16.2356C1.97425 16.0968 2.07146 15.9168 2.26588 15.557L3.62893 13.034C3.74118 12.8262 3.79731 12.7223 3.82273 12.6225C3.84784 12.5238 3.85391 12.4527 3.84588 12.3512C3.83775 12.2484 3.79266 12.1147 3.7025 11.8472C3.46289 11.1363 3.33302 10.375 3.33302 9.58333C3.33302 5.67132 6.50434 2.5 10.4164 2.5C14.3284 2.5 17.4997 5.67132 17.4997 9.58333Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">Chat</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Chat</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
<div className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -155,7 +156,7 @@ const Reports: React.FC = () => {
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">Help</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Help</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,23 +172,23 @@ const Reports: React.FC = () => {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">Settings</div>
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Settings</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CTA Card */}
|
{/* CTA Card */}
|
||||||
<div className="self-stretch bg-Neutrals-NeutralSlate0 rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 flex flex-col justify-start items-start overflow-hidden">
|
<div className="self-stretch bg-[--Neutrals-NeutralSlate0] rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 flex flex-col justify-start items-start overflow-hidden">
|
||||||
<div className="self-stretch h-24 relative">
|
<div className="self-stretch h-24 relative">
|
||||||
<div className="w-60 h-32 left-0 top-[-0.50px] absolute bg-gradient-to-b from-black to-black/0" />
|
<div className="w-60 h-32 left-0 top-[-0.50px] absolute bg-gradient-to-b from-black to-black/0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-sm font-semibold font-['Inter'] leading-tight">Build [Company]'s Report</div>
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-sm font-semibold font-['Inter'] leading-tight">Build [Company]'s Report</div>
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-xs font-normal font-['Inter'] leading-none">Share this form with your team members to capture valuable info about your company to train Auditly.</div>
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-xs font-normal font-['Inter'] leading-none">Share this form with your team members to capture valuable info about your company to train Auditly.</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||||
<div className="flex-1 px-3 py-1.5 bg-Button-Secondary rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
<div className="flex-1 px-3 py-1.5 bg-Button-Secondary rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -195,7 +196,7 @@ const Reports: React.FC = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-3 py-1.5 bg-Brand-Orange rounded-[999px] outline outline-1 outline-offset-[-1px] outline-blue-400 flex justify-center items-center gap-0.5 overflow-hidden">
|
<div className="flex-1 px-3 py-1.5 bg-[--Brand-Orange] rounded-[999px] outline outline-1 outline-offset-[-1px] outline-blue-400 flex justify-center items-center gap-0.5 overflow-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8.97171 12.2442L8.0289 13.1871C6.72716 14.4888 4.61661 14.4888 3.31486 13.1871C2.01311 11.8853 2.01311 9.77476 3.31486 8.47301L4.25767 7.5302M12.7429 8.47301L13.6858 7.5302C14.9875 6.22845 14.9875 4.1179 13.6858 2.81615C12.384 1.51441 10.2735 1.51441 8.97171 2.81615L8.0289 3.75896M6.16697 10.3349L10.8336 5.66826" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M8.97171 12.2442L8.0289 13.1871C6.72716 14.4888 4.61661 14.4888 3.31486 13.1871C2.01311 11.8853 2.01311 9.77476 3.31486 8.47301L4.25767 7.5302M12.7429 8.47301L13.6858 7.5302C14.9875 6.22845 14.9875 4.1179 13.6858 2.81615C12.384 1.51441 10.2735 1.51441 8.97171 2.81615L8.0289 3.75896M6.16697 10.3349L10.8336 5.66826" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -212,16 +213,16 @@ const Reports: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Section - Employee List */}
|
{/* Middle Section - Employee List */}
|
||||||
<div className="flex-1 self-stretch bg-Neutrals-NeutralSlate0 flex justify-start items-center">
|
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] flex justify-start items-center">
|
||||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-Outline-Outline-Gray-200 inline-flex flex-col justify-start items-start">
|
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-Outline-Outline-Gray-200 inline-flex flex-col justify-start items-start">
|
||||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate950 text-base font-medium font-['Inter'] leading-normal">Employees</div>
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">Employees</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="self-stretch px-2.5 flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch px-2.5 flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
<div className="self-stretch px-4 py-3 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
<div className="self-stretch px-4 py-3 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M17.5 17.5L14.5834 14.5833M16.6667 9.58333C16.6667 13.4954 13.4954 16.6667 9.58333 16.6667C5.67132 16.6667 2.5 13.4954 2.5 9.58333C2.5 5.67132 5.67132 2.5 9.58333 2.5C13.4954 2.5 16.6667 5.67132 16.6667 9.58333Z" stroke="var(--Neutrals-NeutralSlate500, #717680)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M17.5 17.5L14.5834 14.5833M16.6667 9.58333C16.6667 13.4954 13.4954 16.6667 9.58333 16.6667C5.67132 16.6667 2.5 13.4954 2.5 9.58333C2.5 5.67132 5.67132 2.5 9.58333 2.5C13.4954 2.5 16.6667 5.67132 16.6667 9.58333Z" stroke="var(--Neutrals-NeutralSlate500, #717680)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -232,7 +233,7 @@ const Reports: React.FC = () => {
|
|||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight outline-none"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,12 +248,12 @@ const Reports: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
onClick={handleCompanyReportSelect}
|
onClick={handleCompanyReportSelect}
|
||||||
>
|
>
|
||||||
<div className="w-7 h-7 p-1 bg-Brand-Orange rounded-[666.67px] flex justify-center items-center">
|
<div className="w-7 h-7 p-1 bg-[--Brand-Orange] rounded-[666.67px] flex justify-center items-center">
|
||||||
<div className="text-center justify-start text-white text-xs font-medium font-['Inter'] leading-none">
|
<div className="text-center justify-start text-white text-xs font-medium font-['Inter'] leading-none">
|
||||||
C
|
C
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight">Company Report</div>
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight">Company Report</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -265,11 +266,11 @@ const Reports: React.FC = () => {
|
|||||||
onClick={() => handleEmployeeSelect(employee)}
|
onClick={() => handleEmployeeSelect(employee)}
|
||||||
>
|
>
|
||||||
<div className="w-7 h-7 p-1 bg-Main-BG-Gray-100 rounded-[666.67px] flex justify-center items-center">
|
<div className="w-7 h-7 p-1 bg-Main-BG-Gray-100 rounded-[666.67px] flex justify-center items-center">
|
||||||
<div className="text-center justify-start text-Neutrals-NeutralSlate500 text-xs font-medium font-['Inter'] leading-none">
|
<div className="text-center justify-start text-[--Neutrals-NeutralSlate500] text-xs font-medium font-['Inter'] leading-none">
|
||||||
{employee.initials}
|
{employee.initials}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 text-sm font-normal font-['Inter'] leading-tight">
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">
|
||||||
{employee.name}
|
{employee.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,17 +290,17 @@ const Reports: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmployeeReportContent
|
<EmployeeReportContent
|
||||||
report={selectedReport.report as Report}
|
report={selectedReport.report as EmployeeReport}
|
||||||
employeeName={selectedReport.employeeName!}
|
employeeName={selectedReport.employeeName!}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-Neutrals-NeutralSlate950 mb-2">
|
<h3 className="text-lg font-semibold text-[--Neutrals-NeutralSlate950] mb-2">
|
||||||
Select a Report
|
Select a Report
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-Neutrals-NeutralSlate500">
|
<p className="text-[--Neutrals-NeutralSlate500]">
|
||||||
Choose a company or employee report from the list to view details.
|
Choose a company or employee report from the list to view details.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,14 +319,16 @@ const CompanyReportContent: React.FC<{
|
|||||||
onRegenerate: () => void;
|
onRegenerate: () => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
}> = ({ report, onRegenerate, isGenerating }) => {
|
}> = ({ report, onRegenerate, isGenerating }) => {
|
||||||
|
const [activeDepartmentTab, setActiveDepartmentTab] = useState('Campaigns');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
|
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 text-base font-medium font-['Inter'] leading-normal">
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-medium font-['Inter'] leading-normal">
|
||||||
Company Report
|
Company Report
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-2.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
|
<div className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -374,10 +377,218 @@ const CompanyReportContent: React.FC<{
|
|||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Personnel Changes</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Personnel Changes</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-6">
|
||||||
|
|
||||||
|
{/* New Hires */}
|
||||||
|
{report.personnelChanges.newHires && report.personnelChanges.newHires.length > 0 && (
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">New Hires</div>
|
||||||
|
{report.personnelChanges.newHires.map((hire, index) => (
|
||||||
|
<div key={index} className="self-stretch p-3 bg-green-50 rounded-lg border border-green-200">
|
||||||
|
<div className="font-medium text-green-800">{hire.name} - {hire.role}</div>
|
||||||
|
<div className="text-sm text-green-600">Department: {hire.department}</div>
|
||||||
|
{hire.impact && <div className="text-sm text-green-600 mt-1">{hire.impact}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Promotions */}
|
||||||
|
{report.personnelChanges.promotions && report.personnelChanges.promotions.length > 0 && (
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Promotions</div>
|
||||||
|
{report.personnelChanges.promotions.map((promotion, index) => (
|
||||||
|
<div key={index} className="self-stretch p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<div className="font-medium text-blue-800">{promotion.name}</div>
|
||||||
|
<div className="text-sm text-blue-600">{promotion.fromRole} → {promotion.toRole}</div>
|
||||||
|
{promotion.impact && <div className="text-sm text-blue-600 mt-1">{promotion.impact}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Departures */}
|
||||||
|
{report.personnelChanges.departures && report.personnelChanges.departures.length > 0 && (
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Departures</div>
|
||||||
|
{report.personnelChanges.departures.map((departure, index) => (
|
||||||
|
<div key={index} className="self-stretch p-3 bg-red-50 rounded-lg border border-red-200">
|
||||||
|
<div className="font-medium text-red-800">{departure.name}</div>
|
||||||
|
<div className="text-sm text-red-600">Department: {departure.department}</div>
|
||||||
|
<div className="text-sm text-red-600">Reason: {departure.reason}</div>
|
||||||
|
{departure.impact && <div className="text-sm text-red-600 mt-1">{departure.impact}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No changes message */}
|
||||||
|
{(!report.personnelChanges.newHires || report.personnelChanges.newHires.length === 0) &&
|
||||||
|
(!report.personnelChanges.promotions || report.personnelChanges.promotions.length === 0) &&
|
||||||
|
(!report.personnelChanges.departures || report.personnelChanges.departures.length === 0) && (
|
||||||
|
<div className="self-stretch text-Text-Gray-500 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
No personnel changes to report.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hiring Needs */}
|
||||||
|
{report?.immediateHiringNeeds && report.immediateHiringNeeds.length > 0 && (
|
||||||
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Immediate Hiring Needs</div>
|
||||||
|
</div>
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
{report.immediateHiringNeeds.map((need, index) => (
|
||||||
{report.personnelChanges}
|
<div key={index} className="self-stretch p-4 bg-white rounded-lg border border-Text-Gray-200 flex flex-col justify-start items-start gap-2">
|
||||||
{/* Copilot, we need to implement this so that it works with the defined type, it's not currently implemented correctly. */}
|
<div className="flex justify-between items-start w-full">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">
|
||||||
|
{need.role} - {need.department}
|
||||||
|
</div>
|
||||||
|
<div className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${need.priority === 'High' ? 'bg-red-100 text-red-800' :
|
||||||
|
need.priority === 'Medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
'bg-green-100 text-green-800'
|
||||||
|
}`}>
|
||||||
|
{need.priority} Priority
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-Text-Gray-600 text-sm font-normal font-['Inter'] leading-normal">
|
||||||
|
{need.reasoning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Forward Plan */}
|
||||||
|
{report?.forwardOperatingPlan && (
|
||||||
|
<div className="w-full self-stretch p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Forward Plan</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
{
|
||||||
|
report.forwardOperatingPlan.map((plan, index) => (
|
||||||
|
<div key={index} className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="justify-start text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">{plan.title}</div>
|
||||||
|
<div className="self-stretch h-12 justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
{plan.details.map((detail, idx) => (
|
||||||
|
<p key={idx}>{detail}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch h-0 outline outline-1 outline-offset-[-0.50px] outline-Text-Gray-200" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Strengths goes here
|
||||||
|
*/}
|
||||||
|
{report?.strengths && (
|
||||||
|
<div className="self-stretch p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] inline-flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
{report.strengths.map((strength, idx) => (
|
||||||
|
<div key={idx} className="self-stretch inline-flex justify-start items-start gap-4">
|
||||||
|
<div className="w-6 h-6 relative rounded-[999px] overflow-hidden">
|
||||||
|
<div className="w-6 h-6 left-0 top-0 absolute bg-Other-Green rounded-full" />
|
||||||
|
<div data-svg-wrapper className="left-[5px] top-[5px] absolute">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.6666 3.5L5.24998 9.91667L2.33331 7" stroke="var(--Neutrals-NeutralSlate0, #FDFDFD)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">{strength}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Organizational Impact Summary
|
||||||
|
*/}
|
||||||
|
{report?.organizationalImpactSummary && (
|
||||||
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-6">
|
||||||
|
|
||||||
|
{/* Mission Critical */}
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="flex justify-start items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Mission Critical</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
{report.organizationalImpactSummary.missionCritical.map((employee, index) => (
|
||||||
|
<div key={index} className="self-stretch flex justify-between items-start">
|
||||||
|
<div className="w-48 text-Text-Gray-800 text-sm font-medium font-['Inter'] leading-tight">{employee.employeeName}</div>
|
||||||
|
<div className="flex-1 text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">{employee.description}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highly Valuable */}
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="flex justify-start items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Highly Valuable</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
{report.organizationalImpactSummary.highlyValuable.map((employee, index) => (
|
||||||
|
<div key={index} className="self-stretch flex justify-between items-start">
|
||||||
|
<div className="w-48 text-Text-Gray-800 text-sm font-medium font-['Inter'] leading-tight">{employee.employeeName}</div>
|
||||||
|
<div className="flex-1 text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">{employee.description}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Core Support */}
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="flex justify-start items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Core Support</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
{report.organizationalImpactSummary.coreSupport.map((employee, index) => (
|
||||||
|
<div key={index} className="self-stretch flex justify-between items-start">
|
||||||
|
<div className="w-48 text-Text-Gray-800 text-sm font-medium font-['Inter'] leading-tight">{employee.employeeName}</div>
|
||||||
|
<div className="flex-1 text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">{employee.description}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Low Criticality */}
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="flex justify-start items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Low Criticality</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
{report.organizationalImpactSummary.lowCriticality.map((employee, index) => (
|
||||||
|
<div key={index} className="self-stretch flex justify-between items-start">
|
||||||
|
<div className="w-48 text-Text-Gray-800 text-sm font-medium font-['Inter'] leading-tight">{employee.employeeName}</div>
|
||||||
|
<div className="flex-1 text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">{employee.description}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,33 +601,27 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
<div className="p-1 bg-Text-Gray-200 rounded-full outline outline-1 outline-offset-[-1px] outline-Outline-Outline-Gray-200 inline-flex justify-start items-center gap-1">
|
<div className="p-1 bg-Text-Gray-200 rounded-full outline outline-1 outline-offset-[-1px] outline-Outline-Outline-Gray-200 inline-flex justify-start items-center gap-1">
|
||||||
<div className="px-3 py-1.5 bg-Main-BG-White-0 rounded-full flex justify-center items-center gap-1 overflow-hidden">
|
{/* Department Tabs */}
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
{['Campaigns', 'Social Media', 'Creative', 'Tech', 'Admin/OPS'].map((dept) => (
|
||||||
<div className="justify-start text-Text-Dark-950 text-xs font-medium font-['Inter'] leading-none">Campaigns</div>
|
<div
|
||||||
|
key={dept}
|
||||||
|
className={`px-3 py-1.5 rounded-full flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${activeDepartmentTab === dept
|
||||||
|
? 'bg-Main-BG-White-0'
|
||||||
|
: 'shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveDepartmentTab(dept)}
|
||||||
|
>
|
||||||
|
<div className="px-0.5 flex justify-center items-center">
|
||||||
|
<div className={`justify-start text-xs font-medium font-['Inter'] leading-none ${activeDepartmentTab === dept ? 'text-Text-Dark-950' : 'text-zinc-600'
|
||||||
|
}`}>
|
||||||
|
{dept}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<div className="px-3 py-1.5 rounded-full shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden">
|
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
|
||||||
<div className="justify-start text-zinc-600 text-xs font-medium font-['Inter'] leading-none">Social Media</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-1.5 rounded-full shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden">
|
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
|
||||||
<div className="justify-start text-zinc-600 text-xs font-medium font-['Inter'] leading-none">Creative</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-1.5 rounded-full shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden">
|
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
|
||||||
<div className="justify-start text-zinc-600 text-xs font-medium font-['Inter'] leading-none">Tech</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-1.5 rounded-full shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden">
|
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
|
||||||
<div className="justify-start text-zinc-600 text-xs font-medium font-['Inter'] leading-none">Admin/OPS</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
{/* Department Overview Section */}
|
||||||
<div className="self-stretch p-5 bg-Text-Gray-100 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-3 overflow-hidden">
|
<div className="self-stretch p-5 bg-Text-Gray-100 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-3 overflow-hidden">
|
||||||
<div className="self-stretch inline-flex justify-between items-center">
|
<div className="self-stretch inline-flex justify-between items-center">
|
||||||
<div className="w-48 justify-start text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Department Overview</div>
|
<div className="w-48 justify-start text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Department Overview</div>
|
||||||
@@ -436,42 +641,63 @@ const CompanyReportContent: React.FC<{
|
|||||||
Overall company performance shows strong collaboration and delivery with opportunities for process improvement.
|
Overall company performance shows strong collaboration and delivery with opportunities for process improvement.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Employee Radar Charts Section */}
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Team Performance Analysis</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
|
{report?.gradingBreakdown?.teamScores
|
||||||
|
?.filter(teamScore => {
|
||||||
|
// Filter employees based on active department tab
|
||||||
|
// For now, showing all employees as we don't have department info per employee
|
||||||
|
// In a real implementation, you'd filter by employee department
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
?.map((teamScore, index) => {
|
||||||
|
const radarData = [
|
||||||
|
{ label: 'Reliability', value: teamScore.reliability * 10 },
|
||||||
|
{ label: 'Role Fit', value: teamScore.roleFit * 10 },
|
||||||
|
{ label: 'Scalability', value: teamScore.scalability * 10 },
|
||||||
|
{ label: 'Output', value: teamScore.output * 10 },
|
||||||
|
{ label: 'Initiative', value: teamScore.initiative * 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="p-4 bg-white rounded-lg shadow-sm border border-Text-Gray-200 flex flex-col justify-start items-center gap-3">
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<h3 className="text-base font-semibold text-Text-Gray-800">{teamScore.employeeName}</h3>
|
||||||
|
<div className={`px-2 py-1 rounded-full text-xs font-medium ${teamScore.grade.startsWith('A') ? 'bg-green-100 text-green-800' :
|
||||||
|
teamScore.grade.startsWith('B') ? 'bg-blue-100 text-blue-800' :
|
||||||
|
teamScore.grade.startsWith('C') ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
Grade: {teamScore.grade}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-64">
|
||||||
|
<RadarPerformanceChart
|
||||||
|
data={radarData}
|
||||||
|
height={240}
|
||||||
|
color={
|
||||||
|
teamScore.grade.startsWith('A') ? '#22c55e' :
|
||||||
|
teamScore.grade.startsWith('B') ? '#3b82f6' :
|
||||||
|
teamScore.grade.startsWith('C') ? '#f59e0b' :
|
||||||
|
'#ef4444'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hiring Needs */}
|
|
||||||
{report?.immediateHiringNeeds && (
|
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Hiring Needs</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
{report.immediateHiringNeeds}
|
|
||||||
{/* Copilot, we need to implement this so that it works with the defined type, it's not currently implemented correctly. */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Forward Plan */}
|
|
||||||
{report?.forwardOperatingPlan && (
|
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Forward Plan</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
{report.forwardOperatingPlan}
|
|
||||||
{/* Copilot, we need to implement this so that it works with the defined type, it's not currently implemented correctly. */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Company Strengths */}
|
{/* Company Strengths */}
|
||||||
{report?.organizationalStrengths && (
|
{/* {report?.organizationalStrengths && (
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Company Strengths</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Company Strengths</div>
|
||||||
@@ -494,7 +720,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -502,17 +728,17 @@ const CompanyReportContent: React.FC<{
|
|||||||
|
|
||||||
// Component for displaying employee report content
|
// Component for displaying employee report content
|
||||||
const EmployeeReportContent: React.FC<{
|
const EmployeeReportContent: React.FC<{
|
||||||
report: Report;
|
report: EmployeeReport;
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
}> = ({ report, employeeName }) => {
|
}> = ({ report, employeeName }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
|
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
|
||||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 text-base font-medium font-['Inter'] leading-normal">
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-medium font-['Inter'] leading-normal">
|
||||||
{employeeName}'s Answers
|
{employeeName}'s Answers
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-2.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
|
<div className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -526,7 +752,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
||||||
{/* Performance Overview */}
|
{/* Role & Responsibilities */}
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Role & Responsibilities</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Role & Responsibilities</div>
|
||||||
@@ -538,44 +764,91 @@ const EmployeeReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Work Output */}
|
{/* Self-Rated Output */}
|
||||||
{report.roleAndOutput?.output && (
|
{report.roleAndOutput?.selfRatedOutput && (
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Work Output</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Self-Rated Output</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
{report.roleAndOutput.output}
|
{report.roleAndOutput.selfRatedOutput}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skills & Experience */}
|
{/* Insights */}
|
||||||
{report.skillsAndExperience && (
|
{report.insights && (
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Skills & Experience</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Insights & Traits</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
{report.insights.personalityTraits && (
|
||||||
{report.skillsAndExperience}
|
<div className="self-stretch">
|
||||||
</p>
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal mb-2">Personality Traits</div>
|
||||||
|
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
{report.insights.personalityTraits}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{report.insights.selfAwareness && (
|
||||||
|
<div className="self-stretch">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal mb-2">Self Awareness</div>
|
||||||
|
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
{report.insights.selfAwareness}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{report.insights.growthDesire && (
|
||||||
|
<div className="self-stretch">
|
||||||
|
<div className="text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal mb-2">Growth Desire</div>
|
||||||
|
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
{report.insights.growthDesire}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Communication Style */}
|
{/* Strengths */}
|
||||||
{report.communicationStyle && (
|
{report.strengths && report.strengths.length > 0 && (
|
||||||
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Communication Style</div>
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
<p className="text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
{report.strengths.map((strength, index) => (
|
||||||
{report.communicationStyle}
|
<div key={index} className="self-stretch flex justify-start items-start gap-3">
|
||||||
</p>
|
<div className="w-6 h-6 relative rounded-[999px] overflow-hidden">
|
||||||
|
<div className="w-6 h-6 left-0 top-0 absolute bg-Other-Green rounded-full" />
|
||||||
|
<div className="left-[5px] top-[5px] absolute">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.6666 3.5L5.24998 9.91667L2.33331 7" stroke="var(--Neutrals-NeutralSlate0, #FDFDFD)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">{strength}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
{report.recommendations && report.recommendations.length > 0 && (
|
||||||
|
<div className="w-full p-3 bg-Text-Gray-100 rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Recommendations</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
{report.recommendations.map((recommendation, index) => (
|
||||||
|
<div key={index} className="self-stretch text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||||
|
• {recommendation}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const SubscriptionSetup: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Get updated subscription status
|
// Get updated subscription status
|
||||||
await getSubscriptionStatus(orgId);
|
await getSubscriptionStatus();
|
||||||
|
|
||||||
// Redirect to onboarding to complete organization setup
|
// Redirect to onboarding to complete organization setup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -57,7 +57,7 @@ const SubscriptionSetup: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const { sessionUrl } = await createCheckoutSession(orgId, user.email!);
|
const { sessionUrl } = await createCheckoutSession(user.email!);
|
||||||
|
|
||||||
// Redirect to Stripe Checkout
|
// Redirect to Stripe Checkout
|
||||||
window.location.href = sessionUrl;
|
window.location.href = sessionUrl;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* All data operations go through authenticated cloud functions
|
* All data operations go through authenticated cloud functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Employee, Report, Submission, CompanyReport } from '../types';
|
import { Employee, EmployeeReport, Submission, CompanyReport, UserOrganization, Organization } from '../types';
|
||||||
import { API_URL } from '../constants';
|
import { API_URL } from '../constants';
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
@@ -42,19 +42,62 @@ interface OrgData {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetUserOrganizations {
|
||||||
|
organizations: UserOrganization[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateOrganization {
|
||||||
|
success: boolean;
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
role: "owner" | "admin" | "employee";
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
joinedAt: number;
|
||||||
|
subscription: any;
|
||||||
|
requiresSubscription: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JoinOrganization {
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
role: "owner" | "admin" | "employee";
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
joinedAt: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateCheckoutSession {
|
||||||
|
success: true;
|
||||||
|
sessionId: string;
|
||||||
|
sessionUrl: string;
|
||||||
|
customerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
class SecureApiService {
|
class SecureApiService {
|
||||||
private async makeRequest<T>(
|
private async makeRequest<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||||
data?: any
|
data?: any,
|
||||||
|
requireAuth: boolean = true
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = `${API_URL}/${endpoint}`;
|
const url = `${API_URL}/${endpoint}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add auth token for authenticated requests
|
||||||
|
if (requireAuth) {
|
||||||
|
const authToken = localStorage.getItem('auditly_auth_token');
|
||||||
|
if (!authToken) {
|
||||||
|
throw new Error('Authentication token not found. Please log in again.');
|
||||||
|
}
|
||||||
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
const config: RequestInit = {
|
const config: RequestInit = {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data && method !== 'GET') {
|
if (data && method !== 'GET') {
|
||||||
@@ -65,6 +108,14 @@ class SecureApiService {
|
|||||||
const response = await fetch(url, config);
|
const response = await fetch(url, config);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// Handle auth errors specifically
|
||||||
|
if (requireAuth && (response.status === 401 || response.status === 403)) {
|
||||||
|
// Clear invalid token and redirect to login
|
||||||
|
localStorage.removeItem('auditly_auth_token');
|
||||||
|
localStorage.removeItem('auditly_demo_session');
|
||||||
|
throw new Error('Authentication failed. Please log in again.');
|
||||||
|
}
|
||||||
|
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -83,90 +134,90 @@ class SecureApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Organization Data Methods
|
// Organization Data Methods
|
||||||
async getOrgData(orgId: string, userId: string): Promise<OrgData> {
|
async getOrgData(): Promise<OrgData> {
|
||||||
const response = await this.makeRequest<{ org: OrgData }>(
|
const response = await this.makeRequest<{ org: OrgData }>(
|
||||||
`getOrgData?orgId=${orgId}&userId=${userId}`
|
'getOrgData', 'GET', null, true
|
||||||
);
|
);
|
||||||
return response.org;
|
return response.org;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOrgData(orgId: string, userId: string, data: Partial<OrgData>): Promise<void> {
|
async updateOrgData(data: Partial<OrgData>): Promise<void> {
|
||||||
await this.makeRequest(
|
await this.makeRequest(
|
||||||
'updateOrgData',
|
'updateOrgData',
|
||||||
'PUT',
|
'PUT',
|
||||||
{ orgId, userId, data }
|
{ data }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Employee Methods
|
// Employee Methods
|
||||||
async getEmployees(orgId: string, userId: string): Promise<Employee[]> {
|
async getEmployees(): Promise<Employee[]> {
|
||||||
const response = await this.makeRequest<{ employees: Employee[] }>(
|
const response = await this.makeRequest<{ employees: Employee[] }>(
|
||||||
`getEmployees?orgId=${orgId}&userId=${userId}`
|
'getEmployees'
|
||||||
);
|
);
|
||||||
return response.employees;
|
return response.employees;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertEmployee(orgId: string, userId: string, employeeData: Partial<Employee>): Promise<Employee> {
|
async upsertEmployee(employeeData: Partial<Employee>): Promise<Employee> {
|
||||||
const response = await this.makeRequest<{ employee: Employee }>(
|
const response = await this.makeRequest<{ employee: Employee }>(
|
||||||
'upsertEmployee',
|
'upsertEmployee',
|
||||||
'POST',
|
'POST',
|
||||||
{ orgId, userId, employeeData }
|
{ employeeData }
|
||||||
);
|
);
|
||||||
return response.employee;
|
return response.employee;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submission Methods
|
// Submission Methods
|
||||||
async getSubmissions(orgId: string, userId: string): Promise<Record<string, Submission>> {
|
async getSubmissions(): Promise<Record<string, Submission>> {
|
||||||
const response = await this.makeRequest<{ submissions: Record<string, Submission> }>(
|
const response = await this.makeRequest<{ submissions: Record<string, Submission> }>(
|
||||||
`getSubmissions?orgId=${orgId}&userId=${userId}`
|
'getSubmissions'
|
||||||
);
|
);
|
||||||
return response.submissions;
|
return response.submissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report Methods
|
// Report Methods
|
||||||
async getReports(orgId: string, userId: string): Promise<Record<string, Report>> {
|
async getReports(): Promise<Record<string, EmployeeReport>> {
|
||||||
const response = await this.makeRequest<{ reports: Record<string, Report> }>(
|
const response = await this.makeRequest<{ reports: Record<string, EmployeeReport> }>(
|
||||||
`getReports?orgId=${orgId}&userId=${userId}`
|
'getReports'
|
||||||
);
|
);
|
||||||
return response.reports;
|
return response.reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveReport(orgId: string, userId: string, employeeId: string, reportData: Partial<Report>): Promise<Report> {
|
async saveReport(employeeId: string, reportData: Partial<EmployeeReport>): Promise<EmployeeReport> {
|
||||||
const response = await this.makeRequest<{ report: Report }>(
|
const response = await this.makeRequest<{ report: EmployeeReport }>(
|
||||||
'saveReport',
|
'saveReport',
|
||||||
'POST',
|
'POST',
|
||||||
{ orgId, userId, employeeId, reportData }
|
{ employeeId, reportData }
|
||||||
);
|
);
|
||||||
return response.report;
|
return response.report;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Company Report Methods
|
// Company Report Methods
|
||||||
async getCompanyReports(orgId: string, userId: string): Promise<CompanyReport[]> {
|
async getCompanyReports(): Promise<CompanyReport[]> {
|
||||||
const response = await this.makeRequest<{ reports: CompanyReport[] }>(
|
const response = await this.makeRequest<{ reports: CompanyReport[] }>(
|
||||||
`getCompanyReports?orgId=${orgId}&userId=${userId}`
|
'getCompanyReports'
|
||||||
);
|
);
|
||||||
return response.reports;
|
return response.reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveCompanyReport(orgId: string, report: any): Promise<string> {
|
async saveCompanyReport(report: any): Promise<string> {
|
||||||
const response = await this.makeRequest<{ reportId: string }>(
|
const response = await this.makeRequest<{ reportId: string }>(
|
||||||
'saveCompanyReport',
|
'saveCompanyReport',
|
||||||
'POST',
|
'POST',
|
||||||
{ orgId, report }
|
{ report }
|
||||||
);
|
);
|
||||||
return response.reportId;
|
return response.reportId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing API methods (these already use cloud functions)
|
// Existing API methods (these already use cloud functions)
|
||||||
async sendOTP(email: string, inviteCode?: string) {
|
async sendOTP(email: string, inviteCode?: string) {
|
||||||
return this.makeRequest('sendOTP', 'POST', { email, inviteCode });
|
return this.makeRequest('sendOTP', 'POST', { email, inviteCode }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOTP(email: string, otp: string) {
|
async verifyOTP(email: string, otp: string) {
|
||||||
return this.makeRequest('verifyOTP', 'POST', { email, otp });
|
return this.makeRequest('verifyOTP', 'POST', { email, otp }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createInvitation(orgId: string, name: string, email: string, role?: string, department?: string): Promise<{
|
async createInvitation(name: string, email: string, role?: string, department?: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
code: string;
|
code: string;
|
||||||
employee: Employee;
|
employee: Employee;
|
||||||
@@ -174,7 +225,7 @@ class SecureApiService {
|
|||||||
emailLink: string;
|
emailLink: string;
|
||||||
message: string;
|
message: string;
|
||||||
}> {
|
}> {
|
||||||
return this.makeRequest('createInvitation', 'POST', { orgId, name, email, role, department });
|
return this.makeRequest('createInvitation', 'POST', { name, email, role, department });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvitationStatus(code: string): Promise<{ used: boolean; employee: any; invite?: any } | null> {
|
async getInvitationStatus(code: string): Promise<{ used: boolean; employee: any; invite?: any } | null> {
|
||||||
@@ -189,8 +240,8 @@ class SecureApiService {
|
|||||||
return this.makeRequest('consumeInvitation', 'POST', { code, userId });
|
return this.makeRequest('consumeInvitation', 'POST', { code, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitEmployeeAnswers(orgId: string, employeeId: string, answers: any, inviteCode?: string) {
|
async submitEmployeeAnswers(employeeId: string, answers: any, inviteCode?: string) {
|
||||||
return this.makeRequest('submitEmployeeAnswers', 'POST', { orgId, employeeId, answers, inviteCode });
|
return this.makeRequest('submitEmployeeAnswers', 'POST', { employeeId, answers, inviteCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateEmployeeReport(employee: any, submission: any, companyWiki?: any) {
|
async generateEmployeeReport(employee: any, submission: any, companyWiki?: any) {
|
||||||
@@ -205,30 +256,28 @@ class SecureApiService {
|
|||||||
return this.makeRequest('chat', 'POST', { message, employeeId, context, mentions, attachments });
|
return this.makeRequest('chat', 'POST', { message, employeeId, context, mentions, attachments });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrganization(name: string, userId: string) {
|
async createOrganization(name: string): Promise<CreateOrganization> {
|
||||||
return this.makeRequest('createOrganization', 'POST', { name, userId });
|
return this.makeRequest('createOrganization', 'POST', { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserOrganizations(userId: string) {
|
async getUserOrganizations(): Promise<GetUserOrganizations> {
|
||||||
return this.makeRequest(`getUserOrganizations?userId=${userId}`);
|
return this.makeRequest('getUserOrganizations', 'GET', null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinOrganization(userId: string, inviteCode: string) {
|
async joinOrganization(inviteCode: string): Promise<JoinOrganization> {
|
||||||
return this.makeRequest('joinOrganization', 'POST', { userId, inviteCode });
|
return this.makeRequest('joinOrganization', 'POST', { inviteCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCheckoutSession(orgId: string, userId: string, userEmail: string, priceId?: string) {
|
async createCheckoutSession(userEmail: string, priceId?: string): Promise<CreateCheckoutSession> {
|
||||||
return this.makeRequest('createCheckoutSession', 'POST', { orgId, userId, userEmail, priceId });
|
return this.makeRequest('createCheckoutSession', 'POST', { userEmail, priceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubscriptionStatus(orgId: string) {
|
async getSubscriptionStatus() {
|
||||||
return this.makeRequest(`getSubscriptionStatus?orgId=${orgId}`);
|
return this.makeRequest('getSubscriptionStatus');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image Storage Methods
|
// Image Storage Methods
|
||||||
async uploadImage(
|
async uploadImage(
|
||||||
orgId: string,
|
|
||||||
userId: string,
|
|
||||||
imageData: {
|
imageData: {
|
||||||
collectionName: string;
|
collectionName: string;
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@@ -240,12 +289,10 @@ class SecureApiService {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
): Promise<{ success: boolean; imageId: string }> {
|
): Promise<{ success: boolean; imageId: string }> {
|
||||||
return this.makeRequest('uploadImage', 'POST', { orgId, userId, imageData });
|
return this.makeRequest('uploadImage', 'POST', { imageData });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImage(
|
async getImage(
|
||||||
orgId: string,
|
|
||||||
userId: string,
|
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
documentId: string
|
documentId: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -260,7 +307,7 @@ class SecureApiService {
|
|||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const response = await this.makeRequest<{ image: any }>(
|
const response = await this.makeRequest<{ image: any }>(
|
||||||
`getImage?orgId=${orgId}&userId=${userId}&collectionName=${collectionName}&documentId=${documentId}`
|
`getImage?collectionName=${collectionName}&documentId=${documentId}`
|
||||||
);
|
);
|
||||||
return response.image || null;
|
return response.image || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -270,13 +317,11 @@ class SecureApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteImage(
|
async deleteImage(
|
||||||
orgId: string,
|
|
||||||
userId: string,
|
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
documentId: string
|
documentId: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.makeRequest('deleteImage', 'DELETE', { orgId, userId, collectionName, documentId });
|
await this.makeRequest('deleteImage', 'DELETE', { collectionName, documentId });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete image:', error);
|
console.error('Failed to delete image:', error);
|
||||||
|
|||||||
187
src/types.ts
187
src/types.ts
@@ -17,7 +17,109 @@ export interface Employee {
|
|||||||
isOwner?: boolean; // Company owner/HR access
|
isOwner?: boolean; // Company owner/HR access
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Report {
|
export interface UserOrganization {
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
role: 'owner' | 'admin' | 'employee';
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
joinedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnboardingData {
|
||||||
|
yourName: string;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
companySize: string;
|
||||||
|
mission: string;
|
||||||
|
missionEvolution: string;
|
||||||
|
vision: string;
|
||||||
|
successDefinition: string;
|
||||||
|
coreValues: string;
|
||||||
|
industryReputation: string;
|
||||||
|
growthLimitations: string;
|
||||||
|
leadershipStructure: string;
|
||||||
|
criticalPeople: string;
|
||||||
|
leadershipGaps: string;
|
||||||
|
delegationOpportunities: string;
|
||||||
|
leadershipDevelopment: string;
|
||||||
|
heldOntoTooLong: string;
|
||||||
|
successionConfidence: string;
|
||||||
|
businessSystems: string;
|
||||||
|
personalDependencies: string;
|
||||||
|
operationalFriction: string;
|
||||||
|
workflowEfficiency: string;
|
||||||
|
recurringProblems: string;
|
||||||
|
wouldFixFirst: string;
|
||||||
|
cultureDescription: string;
|
||||||
|
livedValues: string;
|
||||||
|
internalFriction: string;
|
||||||
|
speakUpCulture: string;
|
||||||
|
clearExpectations: string;
|
||||||
|
staffFeedback: string;
|
||||||
|
highPerformerLove: string;
|
||||||
|
customerAcquisition: string;
|
||||||
|
competitiveAdvantage: string;
|
||||||
|
customerLoss: string;
|
||||||
|
customerLoyalty: string;
|
||||||
|
marketingROI: string;
|
||||||
|
overspending: string;
|
||||||
|
growthAccelerator: string;
|
||||||
|
untappedOpportunities: string;
|
||||||
|
newProducts: string;
|
||||||
|
innovationProcess: string;
|
||||||
|
uniquePosition: string;
|
||||||
|
industryDirection: string;
|
||||||
|
disruptionThreats: string;
|
||||||
|
ideaValidation: string;
|
||||||
|
idealPortfolio: string;
|
||||||
|
innovationBalance: string;
|
||||||
|
innovationPartnerships: string;
|
||||||
|
innovationMetrics: string;
|
||||||
|
businessWorries: string;
|
||||||
|
leadershipBlindSpots: string;
|
||||||
|
stressManagement: string;
|
||||||
|
regretfulDecisions: string;
|
||||||
|
leadershipGrowth: string;
|
||||||
|
leadershipStretched: string;
|
||||||
|
sabbaticalRisks: string;
|
||||||
|
companyLegacy: string;
|
||||||
|
yearSuccess: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Organization {
|
||||||
|
name: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
ownerId: string;
|
||||||
|
// Subscription fields (will be populated after Stripe setup)
|
||||||
|
subscription: {
|
||||||
|
status: 'trial' | 'active' | 'past_due' | 'canceled';
|
||||||
|
stripeCustomerId: string | null;
|
||||||
|
stripeSubscriptionId: string | null;
|
||||||
|
currentPeriodStart: number | null;
|
||||||
|
currentPeriodEnd: number | null;
|
||||||
|
trialEnd: number | null;
|
||||||
|
};
|
||||||
|
// Usage tracking
|
||||||
|
usage: {
|
||||||
|
employeeCount: number;
|
||||||
|
reportsGenerated: number;
|
||||||
|
lastReportGeneration: number | null;
|
||||||
|
};
|
||||||
|
// Organization settings
|
||||||
|
settings: {
|
||||||
|
allowedEmployeeCount: number;
|
||||||
|
featuresEnabled: {
|
||||||
|
aiReports: boolean;
|
||||||
|
chat: boolean;
|
||||||
|
analytics: boolean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onboardingData?: OnboardingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeReport {
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
department: string;
|
department: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -109,16 +211,18 @@ export interface CompanyReport {
|
|||||||
averagePerformanceScore?: number;
|
averagePerformanceScore?: number;
|
||||||
riskLevel?: 'Low' | 'Medium' | 'High';
|
riskLevel?: 'Low' | 'Medium' | 'High';
|
||||||
};
|
};
|
||||||
// Personnel lifecycle
|
weaknesses: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}[];
|
||||||
|
// Personnel lifecycle - Categories should be Promoted, Reassigned, Fired
|
||||||
personnelChanges: {
|
personnelChanges: {
|
||||||
newHires: { name: string; department: string; role: string; impact?: string }[];
|
newHires: { name: string; department: string; role: string; impact?: string }[]; // Is the reassignments?
|
||||||
promotions: { name: string; fromRole: string; toRole: string; impact?: string }[];
|
promotions: { name: string; fromRole: string; toRole: string; impact?: string }[];
|
||||||
departures: { name: string; department: string; reason: string; impact?: string }[];
|
departures: { name: string; department: string; reason: string; impact?: string }[];
|
||||||
};
|
};
|
||||||
// Backward-compatible flattened list (optional)
|
|
||||||
keyPersonnelChanges?: Array<{ employeeName: string; role: string; department: string; changeType: 'newHire' | 'promotion' | 'departure'; impact?: string }>;
|
|
||||||
// Hiring / talent gaps
|
// Hiring / talent gaps
|
||||||
immediateHiringNeeds: {
|
immediateHiringNeeds: { // Needs to be used!!
|
||||||
department: string;
|
department: string;
|
||||||
role: string;
|
role: string;
|
||||||
priority: 'High' | 'Medium' | 'Low';
|
priority: 'High' | 'Medium' | 'Low';
|
||||||
@@ -126,21 +230,11 @@ export interface CompanyReport {
|
|||||||
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
||||||
}[];
|
}[];
|
||||||
recommendations: string[];
|
recommendations: string[];
|
||||||
// Operating plan (dual naming for UI compatibility)
|
forwardOperatingPlan?: {
|
||||||
operatingPlan: {
|
title: string;
|
||||||
nextQuarterGoals: string[];
|
details: string[];
|
||||||
keyInitiatives: string[];
|
}[];
|
||||||
resourceNeeds: string[];
|
strengths: string[];
|
||||||
riskMitigation: string[];
|
|
||||||
};
|
|
||||||
forwardOperatingPlan?: { // deprecated; kept for existing calls until phased out
|
|
||||||
quarterlyGoals: string[];
|
|
||||||
resourceNeeds: string[];
|
|
||||||
riskMitigation: string[];
|
|
||||||
};
|
|
||||||
// Strengths / risks
|
|
||||||
organizationalStrengths: Array<{ icon?: string; area: string; description: string }>;
|
|
||||||
organizationalRisks: string[];
|
|
||||||
organizationalImpactSummary?: {
|
organizationalImpactSummary?: {
|
||||||
missionCritical: {
|
missionCritical: {
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
@@ -165,14 +259,51 @@ export interface CompanyReport {
|
|||||||
};
|
};
|
||||||
// Grading: array + map for UI
|
// Grading: array + map for UI
|
||||||
gradingBreakdown: {
|
gradingBreakdown: {
|
||||||
|
departmentNameShort: string; // For the tab label
|
||||||
departmentName: string;
|
departmentName: string;
|
||||||
lead: string;
|
lead: string; // Team lead
|
||||||
support: string;
|
support: string; // Team leads' support person
|
||||||
summary: string;
|
departmentGrade: string;
|
||||||
category: string;
|
executiveSummary: string;
|
||||||
value: number; // 0-100
|
teamScores: {
|
||||||
rationale?: string;
|
employeeName: string;
|
||||||
}[];
|
grade: string;
|
||||||
|
// Each of the following is out of 10, total being 50 points
|
||||||
|
// These gets displayed as radar charts
|
||||||
|
reliability: number;
|
||||||
|
roleFit: number;
|
||||||
|
scalability: number;
|
||||||
|
output: number;
|
||||||
|
initiative: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
// gradingBreakdown: {
|
||||||
|
// departmentName: string;
|
||||||
|
// lead: string;
|
||||||
|
// support: string;
|
||||||
|
// summary: string;
|
||||||
|
// category: string;
|
||||||
|
// value: number; // 0-100
|
||||||
|
// rationale?: string;
|
||||||
|
// }[];
|
||||||
|
|
||||||
|
// Do not use any of the properties below!!! they need to be safely removed
|
||||||
|
organizationalRisks: string[];
|
||||||
|
operatingPlan: { // Do not use!!!
|
||||||
|
nextQuarterGoals: string[];
|
||||||
|
keyInitiatives: string[];
|
||||||
|
resourceNeeds: string[];
|
||||||
|
riskMitigation: string[];
|
||||||
|
};
|
||||||
|
organizationalStrengths: Array<{ icon?: string; area: string; description: string }>; // Do not use!!! needs to be removed
|
||||||
|
keyPersonnelChanges?: Array<{ // Do not use!!
|
||||||
|
employeeName: string;
|
||||||
|
role: string;
|
||||||
|
newRole?: string;
|
||||||
|
department: string;
|
||||||
|
changeType: 'reassignment' | 'promotion' | 'departure';
|
||||||
|
impact?: string;
|
||||||
|
}>;
|
||||||
gradingOverview?: Record<string, number>; // legacy (0-5 scale expected by old UI)
|
gradingOverview?: Record<string, number>; // legacy (0-5 scale expected by old UI)
|
||||||
executiveSummary: string;
|
executiveSummary: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user