diff --git a/.gitignore b/.gitignore index a96bcfc..3940390 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,11 @@ dist-ssr /deprecated /figma-code /*ignore.* -/document.svg \ No newline at end of file +/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 \ No newline at end of file diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 14ff948..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -elixir 1.18.4-otp-28 diff --git a/ASSESSMENT_REPORT.md b/ASSESSMENT_REPORT.md deleted file mode 100644 index b4cd534..0000000 --- a/ASSESSMENT_REPORT.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/EMPLOYEE_FORMS_FIGMA_README.md b/EMPLOYEE_FORMS_FIGMA_README.md deleted file mode 100644 index b662868..0000000 --- a/EMPLOYEE_FORMS_FIGMA_README.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/SECURITY_MIGRATION.md b/SECURITY_MIGRATION.md deleted file mode 100644 index ae5d95c..0000000 --- a/SECURITY_MIGRATION.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/TODOS.md b/TODOS.md deleted file mode 100644 index c8a2b9b..0000000 --- a/TODOS.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/deploy-security.sh b/deploy-security.sh deleted file mode 100644 index b8abb21..0000000 --- a/deploy-security.sh +++ /dev/null @@ -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 "" \ No newline at end of file diff --git a/functions/index.js b/functions/index.js index 8a65022..3bda7f5 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,2206 +1,2297 @@ -const { onRequest } = require("firebase-functions/v2/https"); -const admin = require("firebase-admin"); -const OpenAI = require("openai"); -const Stripe = require("stripe"); - -const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json"); - -admin.initializeApp({ - credential: admin.credential.cert(serviceAccount) -}); -const db = admin.firestore(); - -// Initialize OpenAI if API key is available -const openai = process.env.OPENAI_API_KEY ? new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}) : null; - -// Initialize Stripe if API key is available -const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { - apiVersion: '2024-11-20.acacia', -}) : null; - -const RESPONSE_FORMAT = { - type: "json_schema", - json_schema: { - name: "company_artifacts", - strict: true, - schema: { - type: "object", - additionalProperties: false, - properties: { - companyPerformance: { - type: "object", - additionalProperties: false, - properties: { - summary: { type: "string" }, - metrics: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - name: { type: "string" }, - value: { anyOf: [{ type: "string" }, { type: "number" }] }, - trend: { enum: ["up", "down", "flat"] } - }, - required: ["name", "value", "trend"] - } - } - }, - required: ["summary", "metrics"] - }, - keyPersonnelChanges: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - person: { type: "string" }, - change: { type: "string" }, // e.g. "Promoted to VP Eng" - impact: { type: "string" }, - effectiveDate: { type: "string" } - }, - required: ["person", "change", "impact", "effectiveDate"] - } - }, - immediateHiringNeeds: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - role: { type: "string" }, - urgency: { enum: ["low", "medium", "high"] }, - reason: { type: "string" } - }, - required: ["role", "urgency", "reason"] - } - }, - forwardOperatingPlan: { - type: "object", - additionalProperties: false, - properties: { - nextQuarterObjectives: { type: "array", items: { type: "string" } }, - initiatives: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - name: { type: "string" }, - owner: { type: "string" }, - kpis: { type: "array", items: { type: "string" } } - }, - required: ["name", "owner", "kpis"] - } - }, - risks: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - risk: { type: "string" }, - mitigation: { type: "string" } - }, - required: ["risk", "mitigation"] - } - } - }, - required: ["nextQuarterObjectives", "initiatives", "risks"] - }, - organizationalInsights: { - type: "object", - additionalProperties: false, - properties: { - culture: { type: "string" }, - teamDynamics: { type: "string" }, - blockers: { type: "array", items: { type: "string" } } - }, - required: ["culture", "teamDynamics", "blockers"] - }, - strengths: { type: "array", items: { type: "string" } }, - gradingOverview: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - department: { type: "string" }, - grade: { enum: ["A", "B", "C", "D", "F"] }, - notes: { type: "string" } - }, - required: ["department", "grade", "notes"] - } - } - }, - required: ["companyPerformance", "keyPersonnelChanges", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths", "gradingOverview"] - - } - } -}; - - -// Helper function to generate OTP -const generateOTP = () => { - return Math.floor(100000 + Math.random() * 900000).toString(); -}; - - -// Send OTP Function -exports.sendOTP = onRequest({ cors: true }, async (req, res) => { - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { email, inviteCode } = req.body; - - if (!email) { - return res.status(400).json({ error: "Email is required" }); - } - - try { - // Generate OTP - const otp = generateOTP(); - const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry - - // Store OTP in Firestore - await db.collection("otps").doc(email).set({ - otp, - expiresAt, - attempts: 0, - inviteCode: inviteCode || null, - createdAt: Date.now(), - }); - - // In production, send actual email - console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`); - - res.json({ - success: true, - message: "Verification code sent to your email", - // Always include OTP in emulator mode for testing - otp, - }); - } catch (error) { - console.error("Send OTP error:", error); - res.status(500).json({ error: "Failed to send verification code" }); - } -}); - -// Verify OTP Function -exports.verifyOTP = onRequest({ cors: true }, async (req, res) => { - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { email, otp } = req.body; - - if (!email || !otp) { - return res.status(400).json({ error: "Email and OTP are required" }); - } - - try { - // Retrieve OTP document - const otpDoc = await db.collection("otps").doc(email).get(); - - if (!otpDoc.exists) { - return res.status(400).json({ error: "Invalid verification code" }); - } - - const otpData = otpDoc.data(); - - // Check if OTP is expired - if (Date.now() > otpData.expiresAt) { - await otpDoc.ref.delete(); - return res.status(400).json({ error: "Verification code has expired" }); - } - - // Check if too many attempts - if (otpData.attempts >= 5) { - await otpDoc.ref.delete(); - return res.status(400).json({ error: "Too many failed attempts" }); - } - - // Verify OTP - if (otpData.otp !== otp) { - await otpDoc.ref.update({ - attempts: (otpData.attempts || 0) + 1, - }); - return res.status(400).json({ error: "Invalid verification code" }); - } - - // OTP is valid - clean up and create/find user - await otpDoc.ref.delete(); - - // Generate a unique user ID for this email if it doesn't exist - let userId; - let userDoc; - - // Check if user already exists by email - const existingUserQuery = await db.collection("users") - .where("email", "==", email) - .limit(1) - .get(); - - if (!existingUserQuery.empty) { - // User exists, get their ID - userDoc = existingUserQuery.docs[0]; - userId = userDoc.id; - } else { - // Create new user - userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - userDoc = null; - } - - // Prepare user object for response - const user = { - uid: userId, - email: email, - displayName: email.split("@")[0], - emailVerified: true, - }; - - // Create or update user document in Firestore - const userRef = db.collection("users").doc(userId); - - const userData = { - id: userId, - email: email, - displayName: email.split("@")[0], - emailVerified: true, - lastLoginAt: Date.now(), - }; - - if (!userDoc) { - // Create new user document - userData.createdAt = Date.now(); - await userRef.set(userData); - } else { - // Update existing user with latest login info - await userRef.update({ - lastLoginAt: Date.now(), - }); - } - - // Generate a simple session token (in production, use proper JWT) - const customToken = `session_${userId}_${Date.now()}`; - - // Handle invitation if present - let inviteData = null; - if (otpData.inviteCode) { - try { - const inviteDoc = await db - .collectionGroup("invites") - .where("code", "==", otpData.inviteCode) - .where("status", "==", "pending") - .limit(1) - .get(); - - if (!inviteDoc.empty) { - inviteData = inviteDoc.docs[0].data(); - } - } catch (error) { - console.error("Error fetching invite:", error); - } - } - - res.json({ - success: true, - user, - token: customToken, - invite: inviteData, - }); - } catch (error) { - console.error("Verify OTP error:", error); - res.status(500).json({ error: "Failed to verify code" }); - } -}); - -// Create Invitation Function -exports.createInvitation = onRequest({ cors: true }, async (req, res) => { - - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - 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 { - // Generate invite code - const code = Math.random().toString(36).substring(2, 15); - - // Generate employee ID - const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - // Create employee object for the invite - const employee = { - id: employeeId, - name: name.trim(), - email: email.trim(), - role: role?.trim() || "employee", - department: department?.trim() || "General", - status: "invited", - inviteCode: code - }; - - // Store invitation with employee data - const inviteRef = await db - .collection("orgs") - .doc(orgId) - .collection("invites") - .doc(code); - - await inviteRef.set({ - code, - employee, - email, - orgId, - status: "pending", - createdAt: Date.now(), - expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days - }); - - // Generate invite links - const baseUrl = process.env.CLIENT_URL || 'http://localhost:5173'; - const inviteLink = `${baseUrl}/#/employee-form/${code}`; - const emailLink = `mailto:${email}?subject=You're invited to join our organization&body=Hi ${name},%0A%0AYou've been invited to complete a questionnaire for our organization. Please click the link below to get started:%0A%0A${inviteLink}%0A%0AThis link will expire in 7 days.%0A%0AThank you!`; - - // In production, send actual invitation email - console.log(`📧 Invitation sent to ${email} (${name}) with code: ${code}`); - console.log(`📧 Invite link: ${inviteLink}`); - - res.json({ - success: true, - code, - employee, - inviteLink, - emailLink, - message: "Invitation sent successfully", - }); - } catch (error) { - console.error("Create invitation error:", error); - res.status(500).json({ error: "Failed to create invitation" }); - } -}); - -// Get Invitation Status Function -exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { code } = req.query; - - if (!code) { - return res.status(400).json({ error: "Invitation code is required" }); - } - - try { - const inviteDoc = await db - .collectionGroup("invites") - .where("code", "==", code) - .limit(1) - .get(); - - if (inviteDoc.empty) { - return res.status(404).json({ error: "Invitation not found" }); - } - - const invite = inviteDoc.docs[0].data(); - - // Check if expired - if (Date.now() > invite.expiresAt) { - return res.status(400).json({ error: "Invitation has expired" }); - } - - res.json({ - success: true, - used: invite.status !== 'pending', - employee: invite.employee, - invite, - }); - } catch (error) { - console.error("Get invitation status error:", error); - res.status(500).json({ error: "Failed to get invitation status" }); - } -}); - -// Consume Invitation Function -exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { code, userId } = req.body; - - if (!code) { - return res.status(400).json({ error: "Invitation code is required" }); - } - - try { - const inviteSnapshot = await db - .collectionGroup("invites") - .where("code", "==", code) - .where("status", "==", "pending") - .limit(1) - .get(); - - if (inviteSnapshot.empty) { - return res.status(404).json({ error: "Invitation not found or already used" }); - } - - const inviteDoc = inviteSnapshot.docs[0]; - const invite = inviteDoc.data(); - - // Check if expired - if (Date.now() > invite.expiresAt) { - return res.status(400).json({ error: "Invitation has expired" }); - } - - // Get employee data from the invite - const employee = invite.employee; - if (!employee) { - return res.status(400).json({ error: "Invalid invitation data - missing employee information" }); - } - - // Mark invitation as consumed - await inviteDoc.ref.update({ - status: "consumed", - consumedBy: employee.id, - consumedAt: Date.now(), - }); - - // Add employee to organization using data from invite - await db - .collection("orgs") - .doc(invite.orgId) - .collection("employees") - .doc(employee.id) - .set({ - id: employee.id, - name: employee.name || employee.email.split("@")[0], - email: employee.email, - role: employee.role || "employee", - department: employee.department || "General", - joinedAt: Date.now(), - status: "active", - inviteCode: code, - }); - - res.json({ - success: true, - orgId: invite.orgId, - message: "Invitation consumed successfully", - }); - } catch (error) { - console.error("Consume invitation error:", error); - res.status(500).json({ error: "Failed to consume invitation" }); - } -}); - -// Submit Employee Answers Function -exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { - - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, 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 { - let finalOrgId, finalEmployeeId; - - if (inviteCode) { - // For invite-based submissions, look up the invite to get employee and org data - const inviteSnapshot = await db - .collectionGroup("invites") - .where("code", "==", inviteCode) - .where("status", "==", "consumed") - .limit(1) - .get(); - - if (inviteSnapshot.empty) { - return res.status(404).json({ error: "Invitation not found or not consumed yet" }); - } - - const invite = inviteSnapshot.docs[0].data(); - finalOrgId = invite.orgId; - finalEmployeeId = invite.employee.id; - } else { - // Regular submission - finalOrgId = orgId; - finalEmployeeId = employeeId; - } - - // Store submission - const submissionRef = await db - .collection("orgs") - .doc(finalOrgId) - .collection("submissions") - .doc(finalEmployeeId); - - await submissionRef.set({ - employeeId: finalEmployeeId, - answers, - submittedAt: Date.now(), - status: "completed", - submissionType: inviteCode ? "invite" : "regular", - ...(inviteCode && { inviteCode }) - }); - - res.json({ - success: true, - message: "Employee answers submitted successfully", - }); - } catch (error) { - console.error("Submit employee answers error:", error); - res.status(500).json({ error: "Failed to submit answers" }); - } -}); - -// Generate Employee Report Function -exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { - - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { employee, submission, companyWiki } = req.body; - - if (!employee || !submission) { - return res.status(400).json({ error: "Employee and submission data are required" }); - } - - try { - let report; - - if (openai) { - // Use OpenAI to generate the report - const prompt = ` -You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data: - -Employee Information: -- Name: ${employee.name || employee.email} -- Role: ${employee.role || "Team Member"} -- Department: ${employee.department || "General"} - -Employee Submission Data: -${JSON.stringify(submission, null, 2)} - -Company Context: -${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"} - -Generate a detailed report with the following structure: -- roleAndOutput: Current role assessment and performance rating -- behavioralInsights: Work style, communication, and team dynamics -- strengths: List of employee strengths -- weaknesses: Areas for improvement (mark critical issues) -- opportunities: Growth and development opportunities -- risks: Potential risks or concerns -- recommendations: Specific action items -- grading: Numerical scores for different performance areas - -Return ONLY valid JSON that matches this structure. Be thorough but professional. - `.trim(); - - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format." - }, - { - role: "user", - content: prompt - } - ], - response_format: { type: "json_object" }, - temperature: 0.7, - }); - - const aiResponse = completion.choices[0].message.content; - const parsedReport = JSON.parse(aiResponse); - - report = { - employeeId: employee.id, - generatedAt: Date.now(), - summary: `AI-generated performance analysis for ${employee.name || employee.email}`, - ...parsedReport - }; - } else { - // Fallback to mock report when OpenAI is not available - report = { - employeeId: employee.id, - generatedAt: Date.now(), - summary: `Performance analysis for ${employee.name || employee.email}`, - roleAndOutput: { - currentRole: employee.role || "Team Member", - keyResponsibilities: ["Task completion", "Team collaboration", "Quality delivery"], - performanceRating: 85, - }, - behavioralInsights: { - workStyle: "Collaborative and detail-oriented", - communicationSkills: "Strong verbal and written communication", - teamDynamics: "Positive team player", - }, - strengths: [ - "Excellent problem-solving abilities", - "Strong attention to detail", - "Reliable and consistent performance", - ], - weaknesses: [ - "Could improve time management", - "Needs to be more proactive in meetings", - ], - opportunities: [ - "Leadership development opportunities", - "Cross-functional project involvement", - "Skill enhancement in emerging technologies", - ], - risks: [ - "Potential burnout from heavy workload", - "Limited growth opportunities in current role", - ], - recommendations: [ - "Provide leadership training", - "Assign mentorship role", - "Consider promotion to senior position", - ], - grading: { - overall: 85, - technical: 88, - communication: 82, - teamwork: 90, - leadership: 75, - }, - }; - } - - res.json({ - success: true, - report, - }); - } catch (error) { - console.error("Generate employee report error:", error); - res.status(500).json({ error: "Failed to generate employee report" }); - } -}); - -// Generate Company Wiki Function -exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { org, submissions = [] } = req.body; - - if (!org) { - return res.status(400).json({ error: "Organization data is required" }); - } - - try { - let report, wiki; - - if (openai) { - // Use OpenAI to generate the company report and wiki - db.collection("orgs").doc(orgId) - const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema."; - const user = [ - "Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.", - "Be thorough and professional.", - "", - "Organization Information:", - JSON.stringify(org, null, 2), - "", - "Employee Submissions:", - JSON.stringify(submissions, null, 2) - ].join("\n"); - - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - temperature: 0, // consistency - response_format: RESPONSE_FORMAT, - messages: [ - { role: "system", content: system }, - { role: "user", content: user } - ] - }); - - // content is guaranteed to be schema-conformant JSON - console.log(completion.choices[0].message); - console.log(completion.choices[0].message.content); - const parsed = JSON.parse(completion.choices[0].message.content); - - const report = { - generatedAt: Date.now(), - ...parsed - }; - - const wiki = { - companyName: org?.name ?? parsed.wiki.companyName, - generatedAt: Date.now(), - - }; - - const companyReport = db.collection("orgs").doc(orgId).collection("companyReport"); - await companyReport.set(report); - const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki"); - await companyWiki.set(wiki); - - console.log(report); - console.log(wiki); - - } else { - // Fallback to mock data when OpenAI is not available - report = { - generatedAt: Date.now(), - companyPerformance: { - overallScore: 82, - trend: "improving", - keyMetrics: { - productivity: 85, - satisfaction: 79, - retention: 88, - }, - }, - keyPersonnelChanges: [ - { - type: "promotion", - employee: "John Doe", - details: "Promoted to Senior Developer", - impact: "positive", - }, - ], - immediateHiringNeeds: [ - { - role: "Frontend Developer", - priority: "high", - timeline: "2-4 weeks", - skills: ["React", "TypeScript", "CSS"], - }, - ], - forwardOperatingPlan: { - nextQuarter: "Focus on product development and team expansion", - challenges: ["Scaling infrastructure", "Talent acquisition"], - opportunities: ["New market segments", "Technology partnerships"], - }, - organizationalInsights: { - teamDynamics: "Strong collaboration across departments", - culturalHealth: "Positive and inclusive work environment", - communicationEffectiveness: "Good but could improve cross-team coordination", - }, - strengths: [ - "Strong technical expertise", - "Collaborative team culture", - "Innovative problem-solving approach", - ], - gradingOverview: { - averagePerformance: 82, - topPerformers: 3, - needsImprovement: 1, - departmentBreakdown: { - engineering: 85, - design: 80, - product: 78, - }, - }, - }; - - wiki = { - companyName: org.name, - industry: org.industry, - description: org.description, - mission: org.mission || "To deliver excellent products and services", - values: org.values || ["Innovation", "Teamwork", "Excellence"], - culture: "Collaborative and growth-oriented", - generatedAt: Date.now(), - }; - } - - res.json({ - success: true, - ...report, - ...wiki, - }); - } catch (error) { - console.error("Generate company wiki error:", error); - res.status(500).json({ error: "Failed to generate company wiki" }); - } -}); - -// Chat Function -exports.chat = onRequest({ cors: true }, async (req, res) => { - - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { message, employeeId, context, mentions, attachments } = req.body; - - if (!message) { - return res.status(400).json({ error: "Message is required" }); - } - - try { - let response; - - if (openai) { - // Use OpenAI for chat responses - const systemPrompt = ` -You are an expert HR consultant and business analyst with access to employee performance data and company analytics. -You provide thoughtful, professional advice based on the employee context and company data provided. - -${context ? ` -Current Context: -${JSON.stringify(context, null, 2)} -` : ''} - -${mentions && mentions.length > 0 ? ` -Mentioned Employees: -${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')} -` : ''} - -Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback. - `.trim(); - - // Build the user message content - let userContent = [ - { - type: "text", - text: message - } - ]; - - // Add image attachments if present - if (attachments && attachments.length > 0) { - attachments.forEach(attachment => { - if (attachment.type.startsWith('image/') && attachment.data) { - userContent.push({ - type: "image_url", - image_url: { - url: attachment.data, - detail: "high" - } - }); - } - // For non-image files, add them as text context - else if (attachment.data) { - userContent.push({ - type: "text", - text: `[Attached file: ${attachment.name} (${attachment.type})]` - }); - } - }); - } - - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: systemPrompt - }, - { - role: "user", - content: userContent - } - ], - temperature: 0.7, - max_tokens: 1000, // Increased for more detailed responses when analyzing images - }); - - response = completion.choices[0].message.content; - } else { - // Fallback responses when OpenAI is not available - const attachmentText = attachments && attachments.length > 0 - ? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.` - : ''; - - const responses = [ - `That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`, - `I can see from the employee report that there are opportunities for growth in...${attachmentText}`, - `The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`, - `Based on the performance data, this employee shows strong potential in...${attachmentText}`, - ]; - - response = responses[Math.floor(Math.random() * responses.length)]; - } - - res.json({ - success: true, - response, - timestamp: Date.now(), - }); - } catch (error) { - console.error("Chat error:", error); - res.status(500).json({ error: "Failed to process chat message" }); - } -}); - -// Create Organization Function -exports.createOrganization = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - 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 { - // Generate unique organization ID - const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - // Create comprehensive organization document - const orgData = { - name, - createdAt: Date.now(), - updatedAt: Date.now(), - onboardingCompleted: false, - ownerId: userId, - // Subscription fields (will be populated after Stripe setup) - subscription: { - status: 'trial', // trial, active, past_due, canceled - stripeCustomerId: null, - stripeSubscriptionId: null, - currentPeriodStart: null, - currentPeriodEnd: null, - trialEnd: Date.now() + (14 * 24 * 60 * 60 * 1000), // 14 day trial - }, - // Usage tracking - usage: { - employeeCount: 0, - reportsGenerated: 0, - lastReportGeneration: null, - }, - // Organization settings - settings: { - allowedEmployeeCount: 50, // Default limit - featuresEnabled: { - aiReports: true, - chat: true, - analytics: true, - } - } - }; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.set(orgData); - - // Get user information from Firestore (since we don't use Firebase Auth) - const userRef = db.collection("users").doc(userId); - const userDoc = await userRef.get(); - - if (!userDoc.exists) { - console.error("User document not found:", userId); - return res.status(400).json({ error: "User not found" }); - } - - const userData = userDoc.data(); - - // Add user as owner to organization's employees collection - const employeeRef = orgRef.collection("employees").doc(userId); - await employeeRef.set({ - id: userId, - role: "owner", - isOwner: true, - joinedAt: Date.now(), - status: "active", - name: userData.displayName || userData.email.split("@")[0], - email: userData.email, - department: "Management", - }); - - // Add organization to user's organizations (for multi-org support) - const userOrgRef = db.collection("users").doc(userId).collection("organizations").doc(orgId); - await userOrgRef.set({ - orgId, - name, - role: "owner", - onboardingCompleted: false, - joinedAt: Date.now(), - }); - - // Update user document with latest activity - await userRef.update({ - lastLoginAt: Date.now(), - }); - - res.json({ - success: true, - orgId, - name, - role: "owner", - onboardingCompleted: false, - joinedAt: Date.now(), - subscription: orgData.subscription, - requiresSubscription: true, // Signal frontend to show subscription flow - }); - } catch (error) { - console.error("Create organization error:", error); - res.status(500).json({ error: "Failed to create organization" }); - } -}); - -// Get User Organizations Function -exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => { - - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Get user's organizations - const userOrgsSnapshot = await db - .collection("users") - .doc(userId) - .collection("organizations") - .get(); - - const organizations = []; - userOrgsSnapshot.forEach(doc => { - organizations.push({ - orgId: doc.id, - ...doc.data(), - }); - }); - - res.json({ - success: true, - organizations, - }); - } catch (error) { - console.error("Get user organizations error:", error); - res.status(500).json({ error: "Failed to get user organizations" }); - } -}); - -// Join Organization Function (via invite) -exports.joinOrganization = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - 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 { - // Find the invitation - const inviteSnapshot = await db - .collectionGroup("invites") - .where("code", "==", inviteCode) - .where("status", "==", "pending") - .limit(1) - .get(); - - if (inviteSnapshot.empty) { - return res.status(404).json({ error: "Invitation not found or already used" }); - } - - const inviteDoc = inviteSnapshot.docs[0]; - const invite = inviteDoc.data(); - - // Check if expired - if (Date.now() > invite.expiresAt) { - return res.status(400).json({ error: "Invitation has expired" }); - } - - const orgId = invite.orgId; - - // Get organization details - const orgDoc = await db.collection("orgs").doc(orgId).get(); - if (!orgDoc.exists()) { - return res.status(404).json({ error: "Organization not found" }); - } - - const orgData = orgDoc.data(); - - // Get user information from Firestore (since we don't use Firebase Auth) - const userRef = db.collection("users").doc(userId); - const userDoc = await userRef.get(); - - if (!userDoc.exists) { - console.error("User document not found:", userId); - return res.status(400).json({ error: "User not found" }); - } - - const userData = userDoc.data(); - - // Mark invitation as consumed - await inviteDoc.ref.update({ - status: "consumed", - consumedBy: userId, - consumedAt: Date.now(), - }); - - // Add user to organization employees with full information - await db - .collection("orgs") - .doc(orgId) - .collection("employees") - .doc(userId) - .set({ - id: userId, - email: userData.email, - name: userData.displayName || userData.email.split("@")[0], - role: invite.role || "employee", - joinedAt: Date.now(), - status: "active", - }); - - // Add organization to user's organizations - await db - .collection("users") - .doc(userId) - .collection("organizations") - .doc(orgId) - .set({ - orgId, - name: orgData.name, - role: invite.role || "employee", - onboardingCompleted: orgData.onboardingCompleted || false, - joinedAt: Date.now(), - }); - - // Update user document with latest login activity - await userRef.update({ - lastLoginAt: Date.now(), - }); - - res.json({ - success: true, - orgId, - name: orgData.name, - role: invite.role || "employee", - onboardingCompleted: orgData.onboardingCompleted || false, - joinedAt: Date.now(), - }); - } catch (error) { - console.error("Join organization error:", error); - res.status(500).json({ error: "Failed to join organization" }); - } -}); - -// Create Stripe Checkout Session Function -exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - 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 { - // Get or create Stripe customer - let customer; - const existingCustomers = await stripe.customers.list({ - email: userEmail, - limit: 1, - }); - - if (existingCustomers.data.length > 0) { - customer = existingCustomers.data[0]; - } else { - customer = await stripe.customers.create({ - email: userEmail, - metadata: { - userId, - orgId, - }, - }); - } - - // Default to standard plan if no priceId provided - const defaultPriceId = priceId || process.env.STRIPE_PRICE_ID || 'price_standard_monthly'; - - // Create checkout session - const session = await stripe.checkout.sessions.create({ - customer: customer.id, - payment_method_types: ['card'], - line_items: [ - { - price: defaultPriceId, - quantity: 1, - }, - ], - mode: 'subscription', - success_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?canceled=true`, - metadata: { - orgId, - userId, - }, - subscription_data: { - metadata: { - orgId, - userId, - }, - trial_period_days: 14, // 14-day trial - }, - }); - - // Update organization with customer ID - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.stripeCustomerId': customer.id, - 'subscription.checkoutSessionId': session.id, - updatedAt: Date.now(), - }); - - res.json({ - success: true, - sessionId: session.id, - sessionUrl: session.url, - customerId: customer.id, - }); - } catch (error) { - console.error("Create checkout session error:", error); - res.status(500).json({ error: "Failed to create checkout session" }); - } -}); - -// Handle Stripe Webhook Function -exports.stripeWebhook = onRequest(async (req, res) => { - if (!stripe) { - return res.status(500).send('Stripe not configured'); - } - - const sig = req.headers['stripe-signature']; - const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; - - let event; - - try { - event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret); - } catch (err) { - console.error('Webhook signature verification failed:', err.message); - return res.status(400).send(`Webhook Error: ${err.message}`); - } - - // Handle the event - try { - switch (event.type) { - case 'checkout.session.completed': - await handleCheckoutCompleted(event.data.object); - break; - case 'customer.subscription.created': - await handleSubscriptionCreated(event.data.object); - break; - case 'customer.subscription.updated': - await handleSubscriptionUpdated(event.data.object); - break; - case 'customer.subscription.deleted': - await handleSubscriptionDeleted(event.data.object); - break; - case 'invoice.payment_succeeded': - await handlePaymentSucceeded(event.data.object); - break; - case 'invoice.payment_failed': - await handlePaymentFailed(event.data.object); - break; - default: - console.log(`Unhandled event type ${event.type}`); - } - - res.json({ received: true }); - } catch (error) { - console.error('Webhook handler error:', error); - res.status(500).json({ error: 'Webhook handler failed' }); - } -}); - -// Get Subscription Status Function -exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - const orgDoc = await db.collection("orgs").doc(orgId).get(); - - if (!orgDoc.exists()) { - return res.status(404).json({ error: "Organization not found" }); - } - - const orgData = orgDoc.data(); - const subscription = orgData.subscription || {}; - - // If we have a Stripe subscription ID, get the latest status - if (stripe && subscription.stripeSubscriptionId) { - try { - const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId); - - // Update local subscription data - await orgDoc.ref.update({ - 'subscription.status': stripeSubscription.status, - 'subscription.currentPeriodStart': stripeSubscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': stripeSubscription.current_period_end * 1000, - updatedAt: Date.now(), - }); - - subscription.status = stripeSubscription.status; - subscription.currentPeriodStart = stripeSubscription.current_period_start * 1000; - subscription.currentPeriodEnd = stripeSubscription.current_period_end * 1000; - } catch (stripeError) { - console.error('Failed to fetch Stripe subscription:', stripeError); - } - } - - res.json({ - success: true, - subscription, - orgId, - }); - } catch (error) { - console.error("Get subscription status error:", error); - res.status(500).json({ error: "Failed to get subscription status" }); - } -}); - -// Webhook Helper Functions -async function handleCheckoutCompleted(session) { - const orgId = session.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'trialing', - 'subscription.stripeSubscriptionId': session.subscription, - 'subscription.checkoutSessionId': session.id, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionCreated(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': subscription.status, - 'subscription.stripeSubscriptionId': subscription.id, - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionUpdated(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': subscription.status, - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionDeleted(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'canceled', - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - updatedAt: Date.now(), - }); -} - -async function handlePaymentSucceeded(invoice) { - const subscriptionId = invoice.subscription; - if (!subscriptionId) return; - - // Update subscription status to active - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - const orgId = subscription.metadata?.orgId; - - if (orgId) { - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'active', - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - updatedAt: Date.now(), - }); - } -} - -async function handlePaymentFailed(invoice) { - const subscriptionId = invoice.subscription; - if (!subscriptionId) return; - - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - const orgId = subscription.metadata?.orgId; - - if (orgId) { - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'past_due', - updatedAt: Date.now(), - }); - } -} - -// exports.helloWorld = onRequest((request, response) => { -// response.send("Hello from Firebase!"); -// }); - -// exports.sendOTP = onRequest(async (request, response) => { -// // Set CORS headers -// response.set('Access-Control-Allow-Origin', '*'); -// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); -// response.set('Access-Control-Allow-Headers', 'Content-Type'); - -// if (request.method === 'OPTIONS') { -// response.status(204).send(''); -// return; -// } - -// if (request.method !== 'POST') { -// response.status(405).json({ error: 'Method not allowed' }); -// return; -// } - -// const { email } = request.body; - -// if (!email) { -// response.status(400).json({ error: 'Email is required' }); -// return; -// } - -// // Generate a simple OTP -// const otp = Math.floor(100000 + Math.random() * 900000).toString(); - -// response.json({ -// success: true, -// message: 'Verification code sent to your email', -// otp: otp // Always return OTP in emulator mode -// }); -// }); - -// exports.verifyOTP = onRequest(async (request, response) => { -// // Set CORS headers -// response.set('Access-Control-Allow-Origin', '*'); -// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); -// response.set('Access-Control-Allow-Headers', 'Content-Type'); - -// if (request.method === 'OPTIONS') { -// response.status(204).send(''); -// return; -// } - -// if (request.method !== 'POST') { -// response.status(405).json({ error: 'Method not allowed' }); -// return; -// } - -// const { email, otp } = request.body; - -// if (!email || !otp) { -// response.status(400).json({ error: 'Email and OTP are required' }); -// return; -// } - -// // Mock verification - accept any 6-digit code -// if (otp.length === 6) { -// response.json({ -// success: true, -// user: { -// uid: 'demo-user-123', -// email: email, -// displayName: email.split('@')[0], -// emailVerified: true -// }, -// token: 'demo-token-123' -// }); -// } else { -// response.status(400).json({ error: 'Invalid verification code' }); -// } -// }); - -// Save Company Report Function -exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, report } = req.body; - - if (!orgId || !report) { - return res.status(400).json({ error: "Organization ID and report are required" }); - } - - try { - // Add ID and timestamp if not present - if (!report.id) { - report.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - if (!report.createdAt) { - report.createdAt = Date.now(); - } - - // Save to Firestore - const reportRef = db.collection("orgs").doc(orgId).collection("fullCompanyReports").doc(report.id); - await reportRef.set(report); - - console.log(`Company report saved successfully for org ${orgId}`); - - res.json({ - success: true, - reportId: report.id, - message: "Company report saved successfully" - }); - } catch (error) { - console.error("Save company report error:", error); - res.status(500).json({ error: "Failed to save company report" }); - } -}); - -// Helper function to verify user authorization -const verifyUserAuthorization = async (userId, orgId) => { - if (!userId || !orgId) { - return false; - } - - try { - // Check if user exists in the organization's employees collection - const employeeDoc = await db.collection("orgs").doc(orgId).collection("employees").doc(userId).get(); - return employeeDoc.exists; - } catch (error) { - console.error("Authorization check error:", error); - return false; - } -}; - -// Get Organization Data Function -exports.getOrgData = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get organization data - const orgDoc = await db.collection("orgs").doc(orgId).get(); - if (!orgDoc.exists) { - return res.status(404).json({ error: "Organization not found" }); - } - - const orgData = { id: orgId, ...orgDoc.data() }; - - res.json({ - success: true, - org: orgData - }); - } catch (error) { - console.error("Get org data error:", error); - res.status(500).json({ error: "Failed to get organization data" }); - } -}); - -// Update Organization Data Function -exports.updateOrgData = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "PUT") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Update organization data - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - ...data, - updatedAt: Date.now() - }); - - res.json({ - success: true, - message: "Organization data updated successfully" - }); - } catch (error) { - console.error("Update org data error:", error); - res.status(500).json({ error: "Failed to update organization data" }); - } -}); - -// Get Employees Function -exports.getEmployees = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get all employees - const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get(); - const employees = []; - - employeesSnapshot.forEach(doc => { - employees.push({ id: doc.id, ...doc.data() }); - }); - - res.json({ - success: true, - employees - }); - } catch (error) { - console.error("Get employees error:", error); - res.status(500).json({ error: "Failed to get employees" }); - } -}); - -// Get Submissions Function -exports.getSubmissions = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get all submissions - const submissionsSnapshot = await db.collection("orgs").doc(orgId).collection("submissions").get(); - const submissions = {}; - - submissionsSnapshot.forEach(doc => { - submissions[doc.id] = { id: doc.id, ...doc.data() }; - }); - - res.json({ - success: true, - submissions - }); - } catch (error) { - console.error("Get submissions error:", error); - res.status(500).json({ error: "Failed to get submissions" }); - } -}); - -// Get Reports Function -exports.getReports = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get all reports - const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("reports").get(); - const reports = {}; - - reportsSnapshot.forEach(doc => { - reports[doc.id] = { id: doc.id, ...doc.data() }; - }); - - res.json({ - success: true, - reports - }); - } catch (error) { - console.error("Get reports error:", error); - res.status(500).json({ error: "Failed to get reports" }); - } -}); - -// Create/Update Employee Function -exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, userId, employeeData } = req.body; - - if (!orgId || !userId || !employeeData) { - return res.status(400).json({ error: "Organization ID, user ID, and employee data are required" }); - } - - try { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Generate employee ID if not provided - if (!employeeData.id) { - employeeData.id = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - - // Add timestamps - const currentTime = Date.now(); - if (!employeeData.createdAt) { - employeeData.createdAt = currentTime; - } - employeeData.updatedAt = currentTime; - - // Save employee - const employeeRef = db.collection("orgs").doc(orgId).collection("employees").doc(employeeData.id); - await employeeRef.set(employeeData); - - res.json({ - success: true, - employee: employeeData, - message: "Employee saved successfully" - }); - } catch (error) { - console.error("Upsert employee error:", error); - res.status(500).json({ error: "Failed to save employee" }); - } -}); - -// Save Report Function -exports.saveReport = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, userId, employeeId, reportData } = req.body; - - if (!orgId || !userId || !employeeId || !reportData) { - return res.status(400).json({ error: "Organization ID, user ID, employee ID, and report data are required" }); - } - - try { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Add metadata - const currentTime = Date.now(); - if (!reportData.id) { - reportData.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - if (!reportData.createdAt) { - reportData.createdAt = currentTime; - } - reportData.updatedAt = currentTime; - reportData.employeeId = employeeId; - - // Save report - const reportRef = db.collection("orgs").doc(orgId).collection("reports").doc(employeeId); - await reportRef.set(reportData); - - res.json({ - success: true, - report: reportData, - message: "Report saved successfully" - }); - } catch (error) { - console.error("Save report error:", error); - res.status(500).json({ error: "Failed to save report" }); - } -}); - -// Get Company Reports Function -exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - 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 { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get all company reports - const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get(); - const reports = []; - - reportsSnapshot.forEach(doc => { - reports.push({ id: doc.id, ...doc.data() }); - }); - - // Sort by creation date (newest first) - reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); - - res.json({ - success: true, - reports - }); - } catch (error) { - console.error("Get company reports error:", error); - res.status(500).json({ error: "Failed to get company reports" }); - } -}); - -// Upload Image Function -exports.uploadImage = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, userId, imageData } = req.body; - - if (!orgId || !userId || !imageData) { - return res.status(400).json({ error: "Organization ID, user ID, and image data are required" }); - } - - try { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Validate image data - const { collectionName, documentId, dataUrl, filename, originalSize, compressedSize, width, height } = imageData; - - if (!collectionName || !documentId || !dataUrl || !filename) { - return res.status(400).json({ error: "Missing required image data fields" }); - } - - // Create image document - const imageDoc = { - id: `${Date.now()}_${filename}`, - dataUrl, - filename, - originalSize: originalSize || 0, - compressedSize: compressedSize || 0, - uploadedAt: Date.now(), - width: width || 0, - height: height || 0, - orgId, - uploadedBy: userId - }; - - // Store image in organization's images collection - const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); - await imageRef.set({ - ...imageDoc, - collectionName, - documentId - }); - - res.json({ - success: true, - imageId: imageDoc.id, - message: "Image uploaded successfully" - }); - } catch (error) { - console.error("Upload image error:", error); - res.status(500).json({ error: "Failed to upload image" }); - } -}); - -// Get Image Function -exports.getImage = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, userId, collectionName, documentId } = req.query; - - if (!orgId || !userId || !collectionName || !documentId) { - return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); - } - - try { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Get image document - const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); - const imageDoc = await imageRef.get(); - - if (!imageDoc.exists) { - return res.status(404).json({ error: "Image not found" }); - } - - const imageData = imageDoc.data(); - - // Return image data (excluding org-specific metadata) - const responseData = { - id: imageData.id, - dataUrl: imageData.dataUrl, - filename: imageData.filename, - originalSize: imageData.originalSize, - compressedSize: imageData.compressedSize, - uploadedAt: imageData.uploadedAt, - width: imageData.width, - height: imageData.height - }; - - res.json({ - success: true, - image: responseData - }); - } catch (error) { - console.error("Get image error:", error); - res.status(500).json({ error: "Failed to get image" }); - } -}); - -// Delete Image Function -exports.deleteImage = onRequest({ cors: true }, async (req, res) => { - if (req.method === 'OPTIONS') { - res.status(204).send(''); - return; - } - - if (req.method !== "DELETE") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const { orgId, userId, collectionName, documentId } = req.body; - - if (!orgId || !userId || !collectionName || !documentId) { - return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); - } - - try { - // Verify user authorization - const isAuthorized = await verifyUserAuthorization(userId, orgId); - if (!isAuthorized) { - return res.status(403).json({ error: "Unauthorized access to organization" }); - } - - // Delete image document - const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); - const imageDoc = await imageRef.get(); - - if (!imageDoc.exists) { - return res.status(404).json({ error: "Image not found" }); - } - - await imageRef.delete(); - - res.json({ - success: true, - message: "Image deleted successfully" - }); - } catch (error) { - console.error("Delete image error:", error); - res.status(500).json({ error: "Failed to delete image" }); - } +const { onRequest } = require("firebase-functions/v2/https"); +const admin = require("firebase-admin"); +const OpenAI = require("openai"); +const Stripe = require("stripe"); + +const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json"); + +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount) +}); +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 +const openai = process.env.OPENAI_API_KEY ? new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}) : null; + +// Initialize Stripe if API key is available +const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2024-11-20.acacia', +}) : null; + +const RESPONSE_FORMAT = { + type: "json_schema", + json_schema: { + name: "company_artifacts", + strict: true, + schema: { + type: "object", + additionalProperties: false, + properties: { + companyPerformance: { + type: "object", + additionalProperties: false, + properties: { + summary: { type: "string" }, + metrics: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { type: "string" }, + value: { anyOf: [{ type: "string" }, { type: "number" }] }, + trend: { enum: ["up", "down", "flat"] } + }, + required: ["name", "value", "trend"] + } + } + }, + required: ["summary", "metrics"] + }, + keyPersonnelChanges: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + person: { type: "string" }, + change: { type: "string" }, // e.g. "Promoted to VP Eng" + impact: { type: "string" }, + effectiveDate: { type: "string" } + }, + required: ["person", "change", "impact", "effectiveDate"] + } + }, + immediateHiringNeeds: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + role: { type: "string" }, + urgency: { enum: ["low", "medium", "high"] }, + reason: { type: "string" } + }, + required: ["role", "urgency", "reason"] + } + }, + forwardOperatingPlan: { + type: "object", + additionalProperties: false, + properties: { + nextQuarterObjectives: { type: "array", items: { type: "string" } }, + initiatives: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { type: "string" }, + owner: { type: "string" }, + kpis: { type: "array", items: { type: "string" } } + }, + required: ["name", "owner", "kpis"] + } + }, + risks: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + risk: { type: "string" }, + mitigation: { type: "string" } + }, + required: ["risk", "mitigation"] + } + } + }, + required: ["nextQuarterObjectives", "initiatives", "risks"] + }, + organizationalInsights: { + type: "object", + additionalProperties: false, + properties: { + culture: { type: "string" }, + teamDynamics: { type: "string" }, + blockers: { type: "array", items: { type: "string" } } + }, + required: ["culture", "teamDynamics", "blockers"] + }, + strengths: { type: "array", items: { type: "string" } }, + gradingOverview: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + department: { type: "string" }, + grade: { enum: ["A", "B", "C", "D", "F"] }, + notes: { type: "string" } + }, + required: ["department", "grade", "notes"] + } + } + }, + required: ["companyPerformance", "keyPersonnelChanges", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths", "gradingOverview"] + + } + } +}; + + +// Helper function to generate OTP +const generateOTP = () => { + return Math.floor(100000 + Math.random() * 900000).toString(); +}; + + +// Send OTP Function +exports.sendOTP = onRequest({ cors: true }, async (req, res) => { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { email, inviteCode } = req.body; + + if (!email) { + return res.status(400).json({ error: "Email is required" }); + } + + try { + // Generate OTP + const otp = generateOTP(); + const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry + + // Store OTP in Firestore + await db.collection("otps").doc(email).set({ + otp, + expiresAt, + attempts: 0, + inviteCode: inviteCode || null, + createdAt: Date.now(), + }); + + // In production, send actual email + console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`); + + res.json({ + success: true, + message: "Verification code sent to your email", + // Always include OTP in emulator mode for testing + otp, + }); + } catch (error) { + console.error("Send OTP error:", error); + res.status(500).json({ error: "Failed to send verification code" }); + } +}); + +// Verify OTP Function +exports.verifyOTP = onRequest({ cors: true }, async (req, res) => { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { email, otp } = req.body; + + if (!email || !otp) { + return res.status(400).json({ error: "Email and OTP are required" }); + } + + try { + // Retrieve OTP document + const otpDoc = await db.collection("otps").doc(email).get(); + + if (!otpDoc.exists) { + return res.status(400).json({ error: "Invalid verification code" }); + } + + const otpData = otpDoc.data(); + + // Check if OTP is expired + if (Date.now() > otpData.expiresAt) { + await otpDoc.ref.delete(); + return res.status(400).json({ error: "Verification code has expired" }); + } + + // Check if too many attempts + if (otpData.attempts >= 5) { + await otpDoc.ref.delete(); + return res.status(400).json({ error: "Too many failed attempts" }); + } + + // Verify OTP + if (otpData.otp !== otp) { + await otpDoc.ref.update({ + attempts: (otpData.attempts || 0) + 1, + }); + return res.status(400).json({ error: "Invalid verification code" }); + } + + // OTP is valid - clean up and create/find user + await otpDoc.ref.delete(); + + // Generate a unique user ID for this email if it doesn't exist + let userId; + let userDoc; + + // Check if user already exists by email + const existingUserQuery = await db.collection("users") + .where("email", "==", email) + .limit(1) + .get(); + + if (!existingUserQuery.empty) { + // User exists, get their ID + userDoc = existingUserQuery.docs[0]; + userId = userDoc.id; + } else { + // Create new user + userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + userDoc = null; + } + + // Prepare user object for response + const user = { + uid: userId, + email: email, + displayName: email.split("@")[0], + emailVerified: true, + }; + + // Create or update user document in Firestore + const userRef = db.collection("users").doc(userId); + + const userData = { + id: userId, + email: email, + displayName: email.split("@")[0], + emailVerified: true, + lastLoginAt: Date.now(), + }; + + if (!userDoc) { + // Create new user document + userData.createdAt = Date.now(); + await userRef.set(userData); + } else { + // Update existing user with latest login info + await userRef.update({ + lastLoginAt: Date.now(), + }); + } + + // Generate a simple session token (in production, use proper JWT) + 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 + let inviteData = null; + if (otpData.inviteCode) { + try { + const inviteDoc = await db + .collectionGroup("invites") + .where("code", "==", otpData.inviteCode) + .where("status", "==", "pending") + .limit(1) + .get(); + + if (!inviteDoc.empty) { + inviteData = inviteDoc.docs[0].data(); + } + } catch (error) { + console.error("Error fetching invite:", error); + } + } + + res.json({ + success: true, + user, + token: customToken, + invite: inviteData, + }); + } catch (error) { + console.error("Verify OTP error:", error); + res.status(500).json({ error: "Failed to verify code" }); + } +}); + +// Create Invitation Function +exports.createInvitation = onRequest({ cors: true }, async (req, res) => { + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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 + const code = Math.random().toString(36).substring(2, 15); + + // Generate employee ID + const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Create employee object for the invite + const employee = { + id: employeeId, + name: name.trim(), + email: email.trim(), + role: role?.trim() || "employee", + department: department?.trim() || "General", + status: "invited", + inviteCode: code + }; + + // Store invitation with employee data + const inviteRef = await db + .collection("orgs") + .doc(orgId) + .collection("invites") + .doc(code); + + await inviteRef.set({ + code, + employee, + email, + orgId, + status: "pending", + createdAt: Date.now(), + expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days + }); + + // Generate invite links + const baseUrl = process.env.CLIENT_URL || 'http://localhost:5173'; + const inviteLink = `${baseUrl}/#/employee-form/${code}`; + const emailLink = `mailto:${email}?subject=You're invited to join our organization&body=Hi ${name},%0A%0AYou've been invited to complete a questionnaire for our organization. Please click the link below to get started:%0A%0A${inviteLink}%0A%0AThis link will expire in 7 days.%0A%0AThank you!`; + + // In production, send actual invitation email + console.log(`📧 Invitation sent to ${email} (${name}) with code: ${code}`); + console.log(`📧 Invite link: ${inviteLink}`); + + res.json({ + success: true, + code, + employee, + inviteLink, + emailLink, + message: "Invitation sent successfully", + }); + } catch (error) { + console.error("Create invitation error:", error); + res.status(500).json({ error: "Failed to create invitation" }); + } +}); + +// Get Invitation Status Function +exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { code } = req.query; + + if (!code) { + return res.status(400).json({ error: "Invitation code is required" }); + } + + try { + const inviteDoc = await db + .collectionGroup("invites") + .where("code", "==", code) + .limit(1) + .get(); + + if (inviteDoc.empty) { + return res.status(404).json({ error: "Invitation not found" }); + } + + const invite = inviteDoc.docs[0].data(); + + // Check if expired + if (Date.now() > invite.expiresAt) { + return res.status(400).json({ error: "Invitation has expired" }); + } + + res.json({ + success: true, + used: invite.status !== 'pending', + employee: invite.employee, + invite, + }); + } catch (error) { + console.error("Get invitation status error:", error); + res.status(500).json({ error: "Failed to get invitation status" }); + } +}); + +// Consume Invitation Function +exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { code, userId } = req.body; + + if (!code) { + return res.status(400).json({ error: "Invitation code is required" }); + } + + try { + const inviteSnapshot = await db + .collectionGroup("invites") + .where("code", "==", code) + .where("status", "==", "pending") + .limit(1) + .get(); + + if (inviteSnapshot.empty) { + return res.status(404).json({ error: "Invitation not found or already used" }); + } + + const inviteDoc = inviteSnapshot.docs[0]; + const invite = inviteDoc.data(); + + // Check if expired + if (Date.now() > invite.expiresAt) { + return res.status(400).json({ error: "Invitation has expired" }); + } + + // Get employee data from the invite + const employee = invite.employee; + if (!employee) { + return res.status(400).json({ error: "Invalid invitation data - missing employee information" }); + } + + // Mark invitation as consumed + await inviteDoc.ref.update({ + status: "consumed", + consumedBy: employee.id, + consumedAt: Date.now(), + }); + + // Add employee to organization using data from invite + await db + .collection("orgs") + .doc(invite.orgId) + .collection("employees") + .doc(employee.id) + .set({ + id: employee.id, + name: employee.name || employee.email.split("@")[0], + email: employee.email, + role: employee.role || "employee", + department: employee.department || "General", + joinedAt: Date.now(), + status: "active", + inviteCode: code, + }); + + res.json({ + success: true, + orgId: invite.orgId, + message: "Invitation consumed successfully", + }); + } catch (error) { + console.error("Consume invitation error:", error); + res.status(500).json({ error: "Failed to consume invitation" }); + } +}); + +// Submit Employee Answers Function +exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { employeeId, answers, inviteCode } = req.body; + + try { + let finalOrgId, finalEmployeeId; + + if (inviteCode) { + // 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 + .collectionGroup("invites") + .where("code", "==", inviteCode) + .where("status", "==", "consumed") + .limit(1) + .get(); + + if (inviteSnapshot.empty) { + return res.status(404).json({ error: "Invitation not found or not consumed yet" }); + } + + const invite = inviteSnapshot.docs[0].data(); + finalOrgId = invite.orgId; + finalEmployeeId = invite.employee.id; + } else { + // Authenticated submission + 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; + + if (!finalOrgId) { + return res.status(400).json({ error: "User has no associated organizations" }); + } + } + + // Store submission + const submissionRef = await db + .collection("orgs") + .doc(finalOrgId) + .collection("submissions") + .doc(finalEmployeeId); + + await submissionRef.set({ + employeeId: finalEmployeeId, + answers, + submittedAt: Date.now(), + status: "completed", + submissionType: inviteCode ? "invite" : "regular", + ...(inviteCode && { inviteCode }) + }); + + res.json({ + success: true, + message: "Employee answers submitted successfully", + }); + } catch (error) { + console.error("Submit employee answers error:", error); + res.status(500).json({ error: "Failed to submit answers" }); + } +}); + +// Generate Employee Report Function +exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { employee, submission, companyWiki } = req.body; + + if (!employee || !submission) { + return res.status(400).json({ error: "Employee and submission data are required" }); + } + + try { + let report; + + if (openai) { + // Use OpenAI to generate the report + const prompt = ` +You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data: + +Employee Information: +- Name: ${employee.name || employee.email} +- Role: ${employee.role || "Team Member"} +- Department: ${employee.department || "General"} + +Employee Submission Data: +${JSON.stringify(submission, null, 2)} + +Company Context: +${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"} + +Generate a detailed report with the following structure: +- roleAndOutput: Current role assessment and performance rating +- behavioralInsights: Work style, communication, and team dynamics +- strengths: List of employee strengths +- weaknesses: Areas for improvement (mark critical issues) +- opportunities: Growth and development opportunities +- risks: Potential risks or concerns +- recommendations: Specific action items +- grading: Numerical scores for different performance areas + +Return ONLY valid JSON that matches this structure. Be thorough but professional. + `.trim(); + + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format." + }, + { + role: "user", + content: prompt + } + ], + response_format: { type: "json_object" }, + temperature: 0.7, + }); + + const aiResponse = completion.choices[0].message.content; + const parsedReport = JSON.parse(aiResponse); + + report = { + employeeId: employee.id, + generatedAt: Date.now(), + summary: `AI-generated performance analysis for ${employee.name || employee.email}`, + ...parsedReport + }; + } else { + // Fallback to mock report when OpenAI is not available + report = { + employeeId: employee.id, + generatedAt: Date.now(), + summary: `Performance analysis for ${employee.name || employee.email}`, + roleAndOutput: { + currentRole: employee.role || "Team Member", + keyResponsibilities: ["Task completion", "Team collaboration", "Quality delivery"], + performanceRating: 85, + }, + behavioralInsights: { + workStyle: "Collaborative and detail-oriented", + communicationSkills: "Strong verbal and written communication", + teamDynamics: "Positive team player", + }, + strengths: [ + "Excellent problem-solving abilities", + "Strong attention to detail", + "Reliable and consistent performance", + ], + weaknesses: [ + "Could improve time management", + "Needs to be more proactive in meetings", + ], + opportunities: [ + "Leadership development opportunities", + "Cross-functional project involvement", + "Skill enhancement in emerging technologies", + ], + risks: [ + "Potential burnout from heavy workload", + "Limited growth opportunities in current role", + ], + recommendations: [ + "Provide leadership training", + "Assign mentorship role", + "Consider promotion to senior position", + ], + grading: { + overall: 85, + technical: 88, + communication: 82, + teamwork: 90, + leadership: 75, + }, + }; + } + + res.json({ + success: true, + report, + }); + } catch (error) { + console.error("Generate employee report error:", error); + res.status(500).json({ error: "Failed to generate employee report" }); + } +}); + +// Generate Company Wiki Function +exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { org, submissions = [] } = req.body; + + if (!org) { + return res.status(400).json({ error: "Organization data is required" }); + } + + try { + let report, wiki; + + if (openai) { + // Use OpenAI to generate the company report and wiki + db.collection("orgs").doc(orgId) + const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema."; + const user = [ + "Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.", + "Be thorough and professional.", + "", + "Organization Information:", + JSON.stringify(org, null, 2), + "", + "Employee Submissions:", + JSON.stringify(submissions, null, 2) + ].join("\n"); + + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + temperature: 0, // consistency + response_format: RESPONSE_FORMAT, + messages: [ + { role: "system", content: system }, + { role: "user", content: user } + ] + }); + + // content is guaranteed to be schema-conformant JSON + console.log(completion.choices[0].message); + console.log(completion.choices[0].message.content); + const parsed = JSON.parse(completion.choices[0].message.content); + + const report = { + generatedAt: Date.now(), + ...parsed + }; + + const wiki = { + companyName: org?.name ?? parsed.wiki.companyName, + generatedAt: Date.now(), + + }; + + const companyReport = db.collection("orgs").doc(orgId).collection("companyReport"); + await companyReport.set(report); + const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki"); + await companyWiki.set(wiki); + + console.log(report); + console.log(wiki); + + } else { + // Fallback to mock data when OpenAI is not available + report = { + generatedAt: Date.now(), + companyPerformance: { + overallScore: 82, + trend: "improving", + keyMetrics: { + productivity: 85, + satisfaction: 79, + retention: 88, + }, + }, + keyPersonnelChanges: [ + { + type: "promotion", + employee: "John Doe", + details: "Promoted to Senior Developer", + impact: "positive", + }, + ], + immediateHiringNeeds: [ + { + role: "Frontend Developer", + priority: "high", + timeline: "2-4 weeks", + skills: ["React", "TypeScript", "CSS"], + }, + ], + forwardOperatingPlan: { + nextQuarter: "Focus on product development and team expansion", + challenges: ["Scaling infrastructure", "Talent acquisition"], + opportunities: ["New market segments", "Technology partnerships"], + }, + organizationalInsights: { + teamDynamics: "Strong collaboration across departments", + culturalHealth: "Positive and inclusive work environment", + communicationEffectiveness: "Good but could improve cross-team coordination", + }, + strengths: [ + "Strong technical expertise", + "Collaborative team culture", + "Innovative problem-solving approach", + ], + gradingOverview: { + averagePerformance: 82, + topPerformers: 3, + needsImprovement: 1, + departmentBreakdown: { + engineering: 85, + design: 80, + product: 78, + }, + }, + }; + + wiki = { + companyName: org.name, + industry: org.industry, + description: org.description, + mission: org.mission || "To deliver excellent products and services", + values: org.values || ["Innovation", "Teamwork", "Excellence"], + culture: "Collaborative and growth-oriented", + generatedAt: Date.now(), + }; + } + + res.json({ + success: true, + ...report, + ...wiki, + }); + } catch (error) { + console.error("Generate company wiki error:", error); + res.status(500).json({ error: "Failed to generate company wiki" }); + } +}); + +// Chat Function +exports.chat = onRequest({ cors: true }, async (req, res) => { + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { message, employeeId, context, mentions, attachments } = req.body; + + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + + try { + let response; + + if (openai) { + // Use OpenAI for chat responses + const systemPrompt = ` +You are an expert HR consultant and business analyst with access to employee performance data and company analytics. +You provide thoughtful, professional advice based on the employee context and company data provided. + +${context ? ` +Current Context: +${JSON.stringify(context, null, 2)} +` : ''} + +${mentions && mentions.length > 0 ? ` +Mentioned Employees: +${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')} +` : ''} + +Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback. + `.trim(); + + // Build the user message content + let userContent = [ + { + type: "text", + text: message + } + ]; + + // Add image attachments if present + if (attachments && attachments.length > 0) { + attachments.forEach(attachment => { + if (attachment.type.startsWith('image/') && attachment.data) { + userContent.push({ + type: "image_url", + image_url: { + url: attachment.data, + detail: "high" + } + }); + } + // For non-image files, add them as text context + else if (attachment.data) { + userContent.push({ + type: "text", + text: `[Attached file: ${attachment.name} (${attachment.type})]` + }); + } + }); + } + + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: systemPrompt + }, + { + role: "user", + content: userContent + } + ], + temperature: 0.7, + max_tokens: 1000, // Increased for more detailed responses when analyzing images + }); + + response = completion.choices[0].message.content; + } else { + // Fallback responses when OpenAI is not available + const attachmentText = attachments && attachments.length > 0 + ? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.` + : ''; + + const responses = [ + `That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`, + `I can see from the employee report that there are opportunities for growth in...${attachmentText}`, + `The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`, + `Based on the performance data, this employee shows strong potential in...${attachmentText}`, + ]; + + response = responses[Math.floor(Math.random() * responses.length)]; + } + + res.json({ + success: true, + response, + timestamp: Date.now(), + }); + } catch (error) { + console.error("Chat error:", error); + res.status(500).json({ error: "Failed to process chat message" }); + } +}); + +// Create Organization Function +exports.createOrganization = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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 + const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Create comprehensive organization document + const orgData = { + name, + createdAt: Date.now(), + updatedAt: Date.now(), + onboardingCompleted: false, + ownerId: authContext.userId, + // Subscription fields (will be populated after Stripe setup) + subscription: { + status: 'trial', // trial, active, past_due, canceled + stripeCustomerId: null, + stripeSubscriptionId: null, + currentPeriodStart: null, + currentPeriodEnd: null, + trialEnd: Date.now() + (14 * 24 * 60 * 60 * 1000), // 14 day trial + }, + // Usage tracking + usage: { + employeeCount: 0, + reportsGenerated: 0, + lastReportGeneration: null, + }, + // Organization settings + settings: { + allowedEmployeeCount: 50, // Default limit + featuresEnabled: { + aiReports: true, + chat: true, + analytics: true, + } + } + }; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.set(orgData); + + // Get user information from Firestore (since we don't use Firebase Auth) + const userRef = db.collection("users").doc(authContext.userId); + const userDoc = await userRef.get(); + + if (!userDoc.exists) { + console.error("User document not found:", authContext.userId); + return res.status(400).json({ error: "User not found" }); + } + + const userData = userDoc.data(); + + // Add user as owner to organization's employees collection + const employeeRef = orgRef.collection("employees").doc(authContext.userId); + await employeeRef.set({ + id: authContext.userId, + role: "owner", + isOwner: true, + joinedAt: Date.now(), + status: "active", + name: userData.displayName || userData.email.split("@")[0], + email: userData.email, + department: "Management", + }); + + // Add organization to user's organizations (for multi-org support) + const userOrgRef = db.collection("users").doc(authContext.userId).collection("organizations").doc(orgId); + await userOrgRef.set({ + orgId, + name, + role: "owner", + onboardingCompleted: false, + joinedAt: Date.now(), + }); + + // Update user document with latest activity + await userRef.update({ + lastLoginAt: Date.now(), + }); + + res.json({ + success: true, + orgId, + name, + role: "owner", + onboardingCompleted: false, + joinedAt: Date.now(), + subscription: orgData.subscription, + requiresSubscription: true, // Signal frontend to show subscription flow + }); + } catch (error) { + console.error("Create organization error:", error); + res.status(500).json({ error: "Failed to create organization" }); + } +}); + +// Get User Organizations Function +exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => { + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + // Validate auth token and get user context + const authContext = await validateAuthAndGetContext(req); + + // Get user's organizations + const userOrgsSnapshot = await db + .collection("users") + .doc(authContext.userId) + .collection("organizations") + .get(); + + const organizations = []; + userOrgsSnapshot.forEach(doc => { + organizations.push({ + orgId: doc.id, + ...doc.data(), + }); + }); + + res.json({ + success: true, + organizations, + }); + } catch (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" }); + } +}); + +// Join Organization Function (via invite) +exports.joinOrganization = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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 + const inviteSnapshot = await db + .collectionGroup("invites") + .where("code", "==", inviteCode) + .where("status", "==", "pending") + .limit(1) + .get(); + + if (inviteSnapshot.empty) { + return res.status(404).json({ error: "Invitation not found or already used" }); + } + + const inviteDoc = inviteSnapshot.docs[0]; + const invite = inviteDoc.data(); + + // Check if expired + if (Date.now() > invite.expiresAt) { + return res.status(400).json({ error: "Invitation has expired" }); + } + + const orgId = invite.orgId; + + // Get organization details + const orgDoc = await db.collection("orgs").doc(orgId).get(); + if (!orgDoc.exists()) { + return res.status(404).json({ error: "Organization not found" }); + } + + const orgData = orgDoc.data(); + + // Get user information from Firestore (since we don't use Firebase Auth) + const userRef = db.collection("users").doc(authContext.userId); + const userDoc = await userRef.get(); + + if (!userDoc.exists) { + console.error("User document not found:", authContext.userId); + return res.status(400).json({ error: "User not found" }); + } + + const userData = userDoc.data(); + + // Mark invitation as consumed + await inviteDoc.ref.update({ + status: "consumed", + consumedBy: authContext.userId, + consumedAt: Date.now(), + }); + + // Add user to organization employees with full information + await db + .collection("orgs") + .doc(orgId) + .collection("employees") + .doc(authContext.userId) + .set({ + id: authContext.userId, + email: userData.email, + name: userData.displayName || userData.email.split("@")[0], + role: invite.role || "employee", + joinedAt: Date.now(), + status: "active", + }); + + // Add organization to user's organizations + await db + .collection("users") + .doc(authContext.userId) + .collection("organizations") + .doc(orgId) + .set({ + orgId, + name: orgData.name, + role: invite.role || "employee", + onboardingCompleted: orgData.onboardingCompleted || false, + joinedAt: Date.now(), + }); + + // Update user document with latest login activity + await userRef.update({ + lastLoginAt: Date.now(), + }); + + res.json({ + success: true, + orgId, + name: orgData.name, + role: invite.role || "employee", + onboardingCompleted: orgData.onboardingCompleted || false, + joinedAt: Date.now(), + }); + } catch (error) { + console.error("Join organization error:", error); + res.status(500).json({ error: "Failed to join organization" }); + } +}); + +// Create Stripe Checkout Session Function +exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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 + let customer; + const existingCustomers = await stripe.customers.list({ + email: userEmail, + limit: 1, + }); + + if (existingCustomers.data.length > 0) { + customer = existingCustomers.data[0]; + } else { + customer = await stripe.customers.create({ + email: userEmail, + metadata: { + userId: authContext.userId, + orgId, + }, + }); + } + + // Default to standard plan if no priceId provided + const defaultPriceId = priceId || process.env.STRIPE_PRICE_ID || 'price_standard_monthly'; + + // Create checkout session + const session = await stripe.checkout.sessions.create({ + customer: customer.id, + payment_method_types: ['card'], + line_items: [ + { + price: defaultPriceId, + quantity: 1, + }, + ], + mode: 'subscription', + success_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?canceled=true`, + metadata: { + orgId, + userId: authContext.userId, + }, + subscription_data: { + metadata: { + orgId, + userId: authContext.userId, + }, + trial_period_days: 14, // 14-day trial + }, + }); + + // Update organization with customer ID + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.stripeCustomerId': customer.id, + 'subscription.checkoutSessionId': session.id, + updatedAt: Date.now(), + }); + + res.json({ + success: true, + sessionId: session.id, + sessionUrl: session.url, + customerId: customer.id, + }); + } catch (error) { + console.error("Create checkout session error:", error); + res.status(500).json({ error: "Failed to create checkout session" }); + } +}); + +// Handle Stripe Webhook Function +exports.stripeWebhook = onRequest(async (req, res) => { + if (!stripe) { + return res.status(500).send('Stripe not configured'); + } + + const sig = req.headers['stripe-signature']; + const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; + + let event; + + try { + event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret); + } catch (err) { + console.error('Webhook signature verification failed:', err.message); + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + // Handle the event + try { + switch (event.type) { + case 'checkout.session.completed': + await handleCheckoutCompleted(event.data.object); + break; + case 'customer.subscription.created': + await handleSubscriptionCreated(event.data.object); + break; + case 'customer.subscription.updated': + await handleSubscriptionUpdated(event.data.object); + break; + case 'customer.subscription.deleted': + await handleSubscriptionDeleted(event.data.object); + break; + case 'invoice.payment_succeeded': + await handlePaymentSucceeded(event.data.object); + break; + case 'invoice.payment_failed': + await handlePaymentFailed(event.data.object); + break; + default: + console.log(`Unhandled event type ${event.type}`); + } + + res.json({ received: true }); + } catch (error) { + console.error('Webhook handler error:', error); + res.status(500).json({ error: 'Webhook handler failed' }); + } +}); + +// Get Subscription Status Function +exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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(); + + if (!orgDoc.exists()) { + return res.status(404).json({ error: "Organization not found" }); + } + + const orgData = orgDoc.data(); + const subscription = orgData.subscription || {}; + + // If we have a Stripe subscription ID, get the latest status + if (stripe && subscription.stripeSubscriptionId) { + try { + const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId); + + // Update local subscription data + await orgDoc.ref.update({ + 'subscription.status': stripeSubscription.status, + 'subscription.currentPeriodStart': stripeSubscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': stripeSubscription.current_period_end * 1000, + updatedAt: Date.now(), + }); + + subscription.status = stripeSubscription.status; + subscription.currentPeriodStart = stripeSubscription.current_period_start * 1000; + subscription.currentPeriodEnd = stripeSubscription.current_period_end * 1000; + } catch (stripeError) { + console.error('Failed to fetch Stripe subscription:', stripeError); + } + } + + res.json({ + success: true, + subscription, + orgId, + }); + } catch (error) { + console.error("Get subscription status error:", error); + res.status(500).json({ error: "Failed to get subscription status" }); + } +}); + +// Webhook Helper Functions +async function handleCheckoutCompleted(session) { + const orgId = session.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'trialing', + 'subscription.stripeSubscriptionId': session.subscription, + 'subscription.checkoutSessionId': session.id, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionCreated(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': subscription.status, + 'subscription.stripeSubscriptionId': subscription.id, + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionUpdated(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': subscription.status, + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionDeleted(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'canceled', + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + updatedAt: Date.now(), + }); +} + +async function handlePaymentSucceeded(invoice) { + const subscriptionId = invoice.subscription; + if (!subscriptionId) return; + + // Update subscription status to active + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const orgId = subscription.metadata?.orgId; + + if (orgId) { + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'active', + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + updatedAt: Date.now(), + }); + } +} + +async function handlePaymentFailed(invoice) { + const subscriptionId = invoice.subscription; + if (!subscriptionId) return; + + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const orgId = subscription.metadata?.orgId; + + if (orgId) { + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'past_due', + updatedAt: Date.now(), + }); + } +} + +// exports.helloWorld = onRequest((request, response) => { +// response.send("Hello from Firebase!"); +// }); + +// exports.sendOTP = onRequest(async (request, response) => { +// // Set CORS headers +// response.set('Access-Control-Allow-Origin', '*'); +// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); +// response.set('Access-Control-Allow-Headers', 'Content-Type'); + +// if (request.method === 'OPTIONS') { +// response.status(204).send(''); +// return; +// } + +// if (request.method !== 'POST') { +// response.status(405).json({ error: 'Method not allowed' }); +// return; +// } + +// const { email } = request.body; + +// if (!email) { +// response.status(400).json({ error: 'Email is required' }); +// return; +// } + +// // Generate a simple OTP +// const otp = Math.floor(100000 + Math.random() * 900000).toString(); + +// response.json({ +// success: true, +// message: 'Verification code sent to your email', +// otp: otp // Always return OTP in emulator mode +// }); +// }); + +// exports.verifyOTP = onRequest(async (request, response) => { +// // Set CORS headers +// response.set('Access-Control-Allow-Origin', '*'); +// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); +// response.set('Access-Control-Allow-Headers', 'Content-Type'); + +// if (request.method === 'OPTIONS') { +// response.status(204).send(''); +// return; +// } + +// if (request.method !== 'POST') { +// response.status(405).json({ error: 'Method not allowed' }); +// return; +// } + +// const { email, otp } = request.body; + +// if (!email || !otp) { +// response.status(400).json({ error: 'Email and OTP are required' }); +// return; +// } + +// // Mock verification - accept any 6-digit code +// if (otp.length === 6) { +// response.json({ +// success: true, +// user: { +// uid: 'demo-user-123', +// email: email, +// displayName: email.split('@')[0], +// emailVerified: true +// }, +// token: 'demo-token-123' +// }); +// } else { +// response.status(400).json({ error: 'Invalid verification code' }); +// } +// }); + +// Save Company Report Function +exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, report } = req.body; + + if (!orgId || !report) { + return res.status(400).json({ error: "Organization ID and report are required" }); + } + + try { + // Add ID and timestamp if not present + if (!report.id) { + report.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + if (!report.createdAt) { + report.createdAt = Date.now(); + } + + // Save to Firestore + const reportRef = db.collection("orgs").doc(orgId).collection("fullCompanyReports").doc(report.id); + await reportRef.set(report); + + console.log(`Company report saved successfully for org ${orgId}`); + + res.json({ + success: true, + reportId: report.id, + message: "Company report saved successfully" + }); + } catch (error) { + console.error("Save company report error:", error); + res.status(500).json({ error: "Failed to save company report" }); + } +}); + +// Helper function to verify user authorization +const verifyUserAuthorization = async (userId, orgId) => { + if (!userId || !orgId) { + return false; + } + + try { + // Check if user exists in the organization's employees collection + const employeeDoc = await db.collection("orgs").doc(orgId).collection("employees").doc(userId).get(); + return employeeDoc.exists; + } catch (error) { + console.error("Authorization check error:", error); + return false; + } +}; + +// Get Organization Data Function +exports.getOrgData = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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" }); + } + + // Get organization data + const orgDoc = await db.collection("orgs").doc(orgId).get(); + if (!orgDoc.exists) { + return res.status(404).json({ error: "Organization not found" }); + } + + const orgData = { id: orgId, ...orgDoc.data() }; + + res.json({ + success: true, + org: orgData + }); + } catch (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" }); + } +}); + +// Update Organization Data Function +exports.updateOrgData = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "PUT") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + // Validate auth token and get user context + const authContext = await validateAuthAndGetContext(req); + const { data } = req.body; + + 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 + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + ...data, + updatedAt: Date.now() + }); + + res.json({ + success: true, + message: "Organization data updated successfully" + }); + } catch (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" }); + } +}); + +// Get Employees Function +exports.getEmployees = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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" }); + } + + // Get all employees + const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get(); + const employees = []; + + employeesSnapshot.forEach(doc => { + employees.push({ id: doc.id, ...doc.data() }); + }); + + res.json({ + success: true, + employees + }); + } catch (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" }); + } +}); + +// Get Submissions Function +exports.getSubmissions = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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" }); + } + + // Get all submissions + const submissionsSnapshot = await db.collection("orgs").doc(orgId).collection("submissions").get(); + const submissions = {}; + + submissionsSnapshot.forEach(doc => { + submissions[doc.id] = { id: doc.id, ...doc.data() }; + }); + + res.json({ + success: true, + submissions + }); + } catch (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" }); + } +}); + +// Get Reports Function +exports.getReports = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + 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" }); + } + + // Get all reports + const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("reports").get(); + const reports = {}; + + reportsSnapshot.forEach(doc => { + reports[doc.id] = { id: doc.id, ...doc.data() }; + }); + + res.json({ + success: true, + reports + }); + } catch (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" }); + } +}); + +// Create/Update Employee Function +exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + + const { orgId, userId, employeeData } = req.body; + + if (!orgId || !userId || !employeeData) { + return res.status(400).json({ error: "Organization ID, user ID, and employee data are required" }); + } + + try { + // Verify user authorization + const isAuthorized = await verifyUserAuthorization(userId, orgId); + if (!isAuthorized) { + return res.status(403).json({ error: "Unauthorized access to organization" }); + } + + // Generate employee ID if not provided + if (!employeeData.id) { + employeeData.id = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // Add timestamps + const currentTime = Date.now(); + if (!employeeData.createdAt) { + employeeData.createdAt = currentTime; + } + employeeData.updatedAt = currentTime; + + // Save employee + const employeeRef = db.collection("orgs").doc(orgId).collection("employees").doc(employeeData.id); + await employeeRef.set(employeeData); + + res.json({ + success: true, + employee: employeeData, + message: "Employee saved successfully" + }); + } catch (error) { + console.error("Upsert employee error:", error); + res.status(500).json({ error: "Failed to save employee" }); + } +}); + +// Save Report Function +exports.saveReport = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, userId, employeeId, reportData } = req.body; + + if (!orgId || !userId || !employeeId || !reportData) { + return res.status(400).json({ error: "Organization ID, user ID, employee ID, and report data are required" }); + } + + try { + // Verify user authorization + const isAuthorized = await verifyUserAuthorization(userId, orgId); + if (!isAuthorized) { + return res.status(403).json({ error: "Unauthorized access to organization" }); + } + + // Add metadata + const currentTime = Date.now(); + if (!reportData.id) { + reportData.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + if (!reportData.createdAt) { + reportData.createdAt = currentTime; + } + reportData.updatedAt = currentTime; + reportData.employeeId = employeeId; + + // Save report + const reportRef = db.collection("orgs").doc(orgId).collection("reports").doc(employeeId); + await reportRef.set(reportData); + + res.json({ + success: true, + report: reportData, + message: "Report saved successfully" + }); + } catch (error) { + console.error("Save report error:", error); + res.status(500).json({ error: "Failed to save report" }); + } +}); + +// Get Company Reports Function +exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const authContext = await validateAuthAndGetContext(req); + + const orgId = authContext.orgId; + if (!orgId) { + return res.status(400).json({ error: "User has no associated organizations" }); + } + + // Get all company reports + const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get(); + const reports = []; + + reportsSnapshot.forEach(doc => { + reports.push({ id: doc.id, ...doc.data() }); + }); + + // Sort by creation date (newest first) + reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); + + res.json({ + success: true, + reports + }); + } catch (error) { + console.error("Get company reports error:", error); + res.status(500).json({ error: "Failed to get company reports" }); + } +}); + +// Upload Image Function +exports.uploadImage = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, userId, imageData } = req.body; + + if (!orgId || !userId || !imageData) { + return res.status(400).json({ error: "Organization ID, user ID, and image data are required" }); + } + + try { + // Verify user authorization + const isAuthorized = await verifyUserAuthorization(userId, orgId); + if (!isAuthorized) { + return res.status(403).json({ error: "Unauthorized access to organization" }); + } + + // Validate image data + const { collectionName, documentId, dataUrl, filename, originalSize, compressedSize, width, height } = imageData; + + if (!collectionName || !documentId || !dataUrl || !filename) { + return res.status(400).json({ error: "Missing required image data fields" }); + } + + // Create image document + const imageDoc = { + id: `${Date.now()}_${filename}`, + dataUrl, + filename, + originalSize: originalSize || 0, + compressedSize: compressedSize || 0, + uploadedAt: Date.now(), + width: width || 0, + height: height || 0, + orgId, + uploadedBy: userId + }; + + // Store image in organization's images collection + const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); + await imageRef.set({ + ...imageDoc, + collectionName, + documentId + }); + + res.json({ + success: true, + imageId: imageDoc.id, + message: "Image uploaded successfully" + }); + } catch (error) { + console.error("Upload image error:", error); + res.status(500).json({ error: "Failed to upload image" }); + } +}); + +// Get Image Function +exports.getImage = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, userId, collectionName, documentId } = req.query; + + if (!orgId || !userId || !collectionName || !documentId) { + return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); + } + + try { + // Verify user authorization + const isAuthorized = await verifyUserAuthorization(userId, orgId); + if (!isAuthorized) { + return res.status(403).json({ error: "Unauthorized access to organization" }); + } + + // Get image document + const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); + const imageDoc = await imageRef.get(); + + if (!imageDoc.exists) { + return res.status(404).json({ error: "Image not found" }); + } + + const imageData = imageDoc.data(); + + // Return image data (excluding org-specific metadata) + const responseData = { + id: imageData.id, + dataUrl: imageData.dataUrl, + filename: imageData.filename, + originalSize: imageData.originalSize, + compressedSize: imageData.compressedSize, + uploadedAt: imageData.uploadedAt, + width: imageData.width, + height: imageData.height + }; + + res.json({ + success: true, + image: responseData + }); + } catch (error) { + console.error("Get image error:", error); + res.status(500).json({ error: "Failed to get image" }); + } +}); + +// Delete Image Function +exports.deleteImage = onRequest({ cors: true }, async (req, res) => { + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "DELETE") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, userId, collectionName, documentId } = req.body; + + if (!orgId || !userId || !collectionName || !documentId) { + return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); + } + + try { + // Verify user authorization + const isAuthorized = await verifyUserAuthorization(userId, orgId); + if (!isAuthorized) { + return res.status(403).json({ error: "Unauthorized access to organization" }); + } + + // Delete image document + const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); + const imageDoc = await imageRef.get(); + + if (!imageDoc.exists) { + return res.status(404).json({ error: "Image not found" }); + } + + await imageRef.delete(); + + res.json({ + success: true, + message: "Image deleted successfully" + }); + } catch (error) { + console.error("Delete image error:", error); + res.status(500).json({ error: "Failed to delete image" }); + } }); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index aa8cfc8..9102103 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { UserOrganizationsProvider, useUserOrganizations } from './contexts/User import { OrgProvider, useOrg } from './contexts/OrgContext'; import { Layout } from './components/UiKit'; import CompanyWiki from './pages/CompanyWiki'; -import EmployeeReport from './pages/EmployeeData'; +// import Report from '../deprecated/pages/EmployeeData'; import Reports from './pages/Reports'; import Chat from './pages/Chat'; import HelpNew from './pages/HelpNew'; @@ -113,7 +113,7 @@ function App() { {/* Employee questionnaire - no auth needed, uses invite code */} } /> } /> - + {/* Legacy employee questionnaire route for backwards compatibility */} } /> @@ -150,7 +150,7 @@ function App() { } /> - + {/* Legacy employee questionnaire route for backwards compatibility */} } /> } /> - } /> + {/* + This is the old reports page + } /> + */} } /> } /> } /> diff --git a/src/components/CompanyWiki/CompanyWikiCompletedState.tsx b/src/components/CompanyWiki/CompanyWikiCompletedState.tsx index 7083e6c..b108369 100644 --- a/src/components/CompanyWiki/CompanyWikiCompletedState.tsx +++ b/src/components/CompanyWiki/CompanyWikiCompletedState.tsx @@ -72,12 +72,12 @@ export const CompanyWikiCompletedState: React.FC 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)]' : ''}`} > -
-
+
+
{sectionNumber}
-
+
{section}
diff --git a/src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx b/src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx index 6715dbb..1aff944 100644 --- a/src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx +++ b/src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx @@ -39,14 +39,14 @@ export const CompanyWikiEmptyState: React.FC = ({ return (
-
-
+
+
{sectionNumber}
-
+
{section}
diff --git a/src/components/CompanyWiki/InviteEmployeesModal.tsx b/src/components/CompanyWiki/InviteEmployeesModal.tsx index 9c10bbc..a9b4442 100644 --- a/src/components/CompanyWiki/InviteEmployeesModal.tsx +++ b/src/components/CompanyWiki/InviteEmployeesModal.tsx @@ -66,7 +66,7 @@ export const InviteEmployeesModal: React.FC = ({
diff --git a/src/components/figma/FigmaEmployeeForms.tsx b/src/components/figma/FigmaEmployeeForms.tsx index ab65572..d331558 100644 --- a/src/components/figma/FigmaEmployeeForms.tsx +++ b/src/components/figma/FigmaEmployeeForms.tsx @@ -19,14 +19,14 @@ export const AuditlyIcon: React.FC = () => ( ); // Progress Bar Component for Section Headers -export const SectionProgressBar: React.FC<{ currentSection: number; totalSections: number; sectionName: string }> = ({ - currentSection, - totalSections, - sectionName +export const SectionProgressBar: React.FC<{ currentSection: number; totalSections: number; sectionName: string }> = ({ + currentSection, + totalSections, + sectionName }) => { return (
-
+
{Array.from({ length: 7 }, (_, index) => { const isActive = index === currentSection - 1; return ( @@ -44,7 +44,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection ); })}
-
+
{sectionName}
@@ -58,27 +58,27 @@ export const WelcomeScreen: React.FC<{ }> = ({ onStart, imageUrl = "https://placehold.co/560x682" }) => { return (
-
+
-
+
-
+
Welcome to the Internal Staff Survey
-
+
The survey takes around 15 minutes to complete.