Fix organization setup flow: redirect to onboarding for incomplete setup
This commit is contained in:
234
contexts/AuthContext.tsx
Normal file
234
contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
||||
import { demoStorage } from '../services/demoStorage';
|
||||
import { API_URL } from '../constants';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signInWithGoogle: () => Promise<void>;
|
||||
signOutUser: () => Promise<void>;
|
||||
signInWithEmail: (email: string, password: string) => Promise<void>;
|
||||
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
||||
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
||||
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
||||
signInWithOTP: (token: string, userData: any) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: check for persisted session
|
||||
console.log('Demo mode: checking for persisted session');
|
||||
const sessionUser = sessionStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring demo session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
}
|
||||
setLoading(false);
|
||||
return () => { };
|
||||
}
|
||||
console.log('Setting up Firebase auth listener');
|
||||
const unsub = onAuthStateChanged(auth, (u) => {
|
||||
console.log('Auth state changed:', u);
|
||||
setUser(u);
|
||||
setLoading(false);
|
||||
});
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// No-op in demo mode
|
||||
return;
|
||||
}
|
||||
await signInWithPopup(auth, googleProvider);
|
||||
};
|
||||
|
||||
const signOutUser = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Clear demo session
|
||||
sessionStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
await signOut(auth);
|
||||
};
|
||||
|
||||
const signInWithEmail = async (email: string, password: string) => {
|
||||
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
if (!isFirebaseConfigured) {
|
||||
console.log('Demo mode: authenticating user', email);
|
||||
const existingUser = demoStorage.getUserByEmail(email);
|
||||
|
||||
if (existingUser) {
|
||||
// Verify password
|
||||
if (demoStorage.verifyPassword(password, existingUser.passwordHash)) {
|
||||
const mockUser = {
|
||||
uid: existingUser.uid,
|
||||
email: existingUser.email,
|
||||
displayName: existingUser.displayName
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
console.log('Demo login successful for:', email);
|
||||
} else {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
} else {
|
||||
throw new Error('User not found. Please sign up first.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log('Attempting Firebase auth');
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
console.error('Firebase Auth Error:', code, e?.message);
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
console.log('Demo mode: creating new user', email);
|
||||
// Check if user already exists
|
||||
const existingUser = demoStorage.getUserByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists with this email');
|
||||
}
|
||||
|
||||
// Create new user
|
||||
const uid = `demo-${btoa(email).slice(0, 8)}`;
|
||||
const newUser = {
|
||||
uid,
|
||||
email,
|
||||
displayName: displayName || email.split('@')[0],
|
||||
passwordHash: demoStorage.hashPassword(password)
|
||||
};
|
||||
|
||||
demoStorage.saveUser(newUser);
|
||||
|
||||
const mockUser = {
|
||||
uid: newUser.uid,
|
||||
email: newUser.email,
|
||||
displayName: newUser.displayName
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
console.log('Demo signup successful for:', email);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cred = await createUserWithEmailAndPassword(auth, email, password);
|
||||
if (displayName) {
|
||||
try { await updateProfile(cred.user, { displayName }); } catch { }
|
||||
}
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const sendOTP = async (email: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/sendOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to send OTP');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/verifyOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otp, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to verify OTP');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Set user in auth context
|
||||
const mockUser = {
|
||||
uid: data.user.uid,
|
||||
email: data.user.email,
|
||||
displayName: data.user.displayName,
|
||||
emailVerified: true
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
sessionStorage.setItem('auditly_auth_token', data.token);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const signInWithOTP = async (token: string, userData: any) => {
|
||||
const mockUser = {
|
||||
uid: userData.uid,
|
||||
email: userData.email,
|
||||
displayName: userData.displayName,
|
||||
emailVerified: true
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
sessionStorage.setItem('auditly_auth_token', token);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
loading,
|
||||
signInWithGoogle,
|
||||
signOutUser,
|
||||
signInWithEmail,
|
||||
signUpWithEmail,
|
||||
sendOTP,
|
||||
verifyOTP,
|
||||
signInWithOTP
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
};
|
||||
799
contexts/OrgContext.tsx
Normal file
799
contexts/OrgContext.tsx
Normal file
@@ -0,0 +1,799 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/firestore';
|
||||
import { db, isFirebaseConfigured } from '../services/firebase';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { Employee, Report, Submission, CompanyReport } from '../types';
|
||||
import { REPORT_DATA, SUBMISSIONS_DATA, SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||
import { demoStorage } from '../services/demoStorage';
|
||||
|
||||
interface OrgData {
|
||||
orgId: string;
|
||||
name: string;
|
||||
industry?: string;
|
||||
size?: string;
|
||||
description?: string;
|
||||
mission?: string;
|
||||
vision?: string;
|
||||
values?: string;
|
||||
foundingYear?: string;
|
||||
evolution?: string;
|
||||
majorMilestones?: string;
|
||||
advantages?: string;
|
||||
vulnerabilities?: string;
|
||||
competitors?: string;
|
||||
marketPosition?: string;
|
||||
currentChallenges?: string;
|
||||
shortTermGoals?: string;
|
||||
longTermGoals?: string;
|
||||
keyMetrics?: string;
|
||||
cultureDescription?: string;
|
||||
workEnvironment?: string;
|
||||
leadershipStyle?: string;
|
||||
communicationStyle?: string;
|
||||
additionalContext?: string;
|
||||
onboardingCompleted?: boolean;
|
||||
}
|
||||
|
||||
interface OrgContextType {
|
||||
org: OrgData | null;
|
||||
orgId: string;
|
||||
employees: Employee[];
|
||||
submissions: Record<string, Submission>;
|
||||
reports: Record<string, Report>;
|
||||
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
||||
saveReport: (employeeId: string, report: Report) => Promise<void>;
|
||||
inviteEmployee: (args: { name: string; email: string }) => Promise<{ employeeId: string; inviteLink: string }>;
|
||||
issueInviteViaApi: (args: { name: string; email: string; role?: string; department?: string }) => Promise<{ code: string; inviteLink: string; emailLink: string; employee: any }>;
|
||||
getInviteStatus: (code: string) => Promise<{ used: boolean; employee: any } | null>;
|
||||
consumeInvite: (code: string) => Promise<{ employee: any } | null>;
|
||||
getReportVersions: (employeeId: string) => Promise<Array<{ id: string; createdAt: number; report: Report }>>;
|
||||
saveReportVersion: (employeeId: string, report: Report) => Promise<void>;
|
||||
acceptInvite: (code: string) => Promise<void>;
|
||||
saveCompanyReport: (summary: string) => Promise<void>;
|
||||
getCompanyReportHistory: () => Promise<Array<{ id: string; createdAt: number; summary: string }>>;
|
||||
saveFullCompanyReport: (report: CompanyReport) => Promise<void>;
|
||||
getFullCompanyReportHistory: () => Promise<CompanyReport[]>;
|
||||
generateCompanyReport: () => Promise<CompanyReport>;
|
||||
generateCompanyWiki: (orgOverride?: OrgData) => Promise<CompanyReport>;
|
||||
seedInitialData: () => Promise<void>;
|
||||
isOwner: (employeeId?: string) => boolean;
|
||||
submitEmployeeAnswers: (employeeId: string, answers: Record<string, string>) => Promise<boolean>;
|
||||
generateEmployeeReport: (employee: Employee) => Promise<Report | null>;
|
||||
getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: Report; error?: string }>;
|
||||
getEmployeeReports: () => Promise<{ success: boolean; reports?: Report[]; error?: string }>;
|
||||
}
|
||||
|
||||
const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
||||
|
||||
export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: string }> = ({ children, selectedOrgId }) => {
|
||||
const { user } = useAuth();
|
||||
const [org, setOrg] = useState<OrgData | null>(null);
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [submissions, setSubmissions] = useState<Record<string, Submission>>({});
|
||||
const [reports, setReports] = useState<Record<string, Report>>({});
|
||||
const [reportVersions, setReportVersions] = useState<Record<string, Array<{ id: string; createdAt: number; report: Report }>>>({});
|
||||
const [companyReports, setCompanyReports] = useState<Array<{ id: string; createdAt: number; summary: string }>>([]);
|
||||
const [fullCompanyReports, setFullCompanyReports] = useState<CompanyReport[]>([]);
|
||||
|
||||
// Use the provided selectedOrgId instead of deriving from user
|
||||
const orgId = selectedOrgId;
|
||||
|
||||
useEffect(() => {
|
||||
console.log('OrgContext effect running, orgId:', orgId, 'isFirebaseConfigured:', isFirebaseConfigured);
|
||||
if (!orgId) return; // Wait for orgId to be available
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode data - use persistent localStorage with proper initialization
|
||||
console.log('Setting up demo org data with persistence');
|
||||
|
||||
// Get or create persistent demo org
|
||||
let demoOrg = demoStorage.getOrganization(orgId);
|
||||
if (!demoOrg) {
|
||||
demoOrg = {
|
||||
orgId: orgId,
|
||||
name: 'Demo Company',
|
||||
onboardingCompleted: false
|
||||
};
|
||||
demoStorage.saveOrganization(demoOrg);
|
||||
|
||||
// Initialize with empty employee list for clean start
|
||||
// (Removed automatic seeding of 6 default employees per user feedback)
|
||||
|
||||
// Create sample submissions for multiple employees
|
||||
const sampleSubmissions = [
|
||||
{
|
||||
employeeId: 'AG',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
answers: {
|
||||
role_clarity: "I understand my role very clearly as Influencer Coordinator & Business Development Outreach.",
|
||||
key_outputs: "Recruited 15 new influencers, managed 8 campaigns, initiated 3 business development partnerships.",
|
||||
bottlenecks: "Campaign organization could be better, sometimes unclear on priorities between recruiting and outreach.",
|
||||
hidden_talent: "Strong relationship building skills that could be leveraged for client-facing work.",
|
||||
retention_risk: "Happy with the company but would like more structure and clearer processes.",
|
||||
energy_distribution: "50% influencer recruiting, 30% campaign support, 20% business development outreach.",
|
||||
performance_indicators: "Good influencer relationships, but delivery timeline improvements needed.",
|
||||
workflow: "Morning outreach, afternoon campaign work, weekly business development calls."
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeId: 'MB',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
answers: {
|
||||
role_clarity: "I understand my role as a Senior Developer very clearly. I'm responsible for architecting solutions, code reviews, and mentoring junior developers.",
|
||||
key_outputs: "Delivered 3 major features this quarter, reduced technical debt by 20%, and led code review process improvements.",
|
||||
bottlenecks: "Sometimes waiting for design specs from the product team, and occasional deployment pipeline issues.",
|
||||
hidden_talent: "I have strong business analysis skills and could help bridge the gap between technical and business requirements.",
|
||||
retention_risk: "I'm satisfied with my current role and compensation. The only concern would be limited growth opportunities.",
|
||||
energy_distribution: "80% development work, 15% mentoring, 5% planning and architecture.",
|
||||
performance_indicators: "Code quality metrics improved, zero production bugs in my recent releases, positive peer feedback.",
|
||||
workflow: "Morning standup, focused coding blocks, afternoon reviews and collaboration, weekly planning sessions."
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeId: 'KT',
|
||||
orgId,
|
||||
createdAt: Date.now(),
|
||||
answers: {
|
||||
role_clarity: "My role as Marketing Manager is clear - I oversee campaigns, analyze performance metrics, and coordinate with sales.",
|
||||
key_outputs: "Launched 5 successful campaigns this quarter, increased lead quality by 30%, improved attribution tracking.",
|
||||
bottlenecks: "Limited budget for premium tools, sometimes slow approval process for creative assets.",
|
||||
hidden_talent: "I have experience with data science and could help build predictive models for customer behavior.",
|
||||
retention_risk: "Overall happy, but would like more strategic input in product positioning and pricing decisions.",
|
||||
energy_distribution: "40% campaign execution, 30% analysis and reporting, 20% strategy, 10% team coordination.",
|
||||
performance_indicators: "Campaign ROI improved by 25%, lead conversion rates increased, better cross-team collaboration.",
|
||||
workflow: "Weekly campaign planning, daily performance monitoring, bi-weekly strategy reviews, monthly board reporting."
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Save all sample submissions
|
||||
sampleSubmissions.forEach(submission => {
|
||||
demoStorage.saveSubmission(submission);
|
||||
});
|
||||
|
||||
// Save sample employee report (only for AG initially)
|
||||
demoStorage.saveEmployeeReport(orgId, REPORT_DATA.employeeId, REPORT_DATA);
|
||||
|
||||
// Save sample company report
|
||||
demoStorage.saveCompanyReport(orgId, SAMPLE_COMPANY_REPORT);
|
||||
}
|
||||
|
||||
// Load persistent demo data
|
||||
setOrg({ orgId, name: demoOrg.name, onboardingCompleted: demoOrg.onboardingCompleted });
|
||||
|
||||
// Convert employees to expected format
|
||||
const demoEmployees = demoStorage.getEmployeesByOrg(orgId);
|
||||
const convertedEmployees: Employee[] = demoEmployees.map(emp => ({
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
email: emp.email,
|
||||
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email.substring(0, 2).toUpperCase(),
|
||||
department: emp.department,
|
||||
role: emp.role,
|
||||
isOwner: emp.id === user?.uid
|
||||
}));
|
||||
setEmployees(convertedEmployees);
|
||||
|
||||
// Convert submissions to expected format
|
||||
const orgSubmissions = demoStorage.getSubmissionsByOrg(orgId);
|
||||
const convertedSubmissions: Record<string, Submission> = {};
|
||||
Object.entries(orgSubmissions).forEach(([employeeId, demoSub]) => {
|
||||
convertedSubmissions[employeeId] = {
|
||||
employeeId,
|
||||
answers: Object.entries(demoSub.answers).map(([question, answer]) => ({
|
||||
question,
|
||||
answer
|
||||
}))
|
||||
};
|
||||
});
|
||||
setSubmissions(convertedSubmissions);
|
||||
|
||||
// Convert reports to expected format
|
||||
const orgReports = demoStorage.getEmployeeReportsByOrg(orgId);
|
||||
setReports(orgReports);
|
||||
|
||||
// Get company reports
|
||||
const companyReports = demoStorage.getCompanyReportsByOrg(orgId);
|
||||
setFullCompanyReports(companyReports);
|
||||
return;
|
||||
}
|
||||
console.log('Setting up Firebase org data');
|
||||
const orgRef = doc(db, 'orgs', orgId);
|
||||
getDoc(orgRef).then(async (snap) => {
|
||||
if (snap.exists()) {
|
||||
setOrg({ orgId, ...(snap.data() as any) });
|
||||
} else {
|
||||
const seed = { name: 'Your Company', onboardingCompleted: false };
|
||||
await setDoc(orgRef, seed);
|
||||
setOrg({ orgId, ...(seed as any) });
|
||||
}
|
||||
});
|
||||
|
||||
const employeesCol = collection(db, 'orgs', orgId, 'employees');
|
||||
const unsubEmp = onSnapshot(employeesCol, (snap) => {
|
||||
const arr: Employee[] = [];
|
||||
snap.forEach((d) => arr.push({ id: d.id, ...(d.data() as any) }));
|
||||
setEmployees(arr);
|
||||
});
|
||||
|
||||
const submissionsCol = collection(db, 'orgs', orgId, 'submissions');
|
||||
const unsubSub = onSnapshot(submissionsCol, (snap) => {
|
||||
const map: Record<string, Submission> = {};
|
||||
snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) }));
|
||||
setSubmissions(map);
|
||||
});
|
||||
|
||||
const reportsCol = collection(db, 'orgs', orgId, 'reports');
|
||||
const unsubRep = onSnapshot(reportsCol, (snap) => {
|
||||
const map: Record<string, Report> = {};
|
||||
snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) } as Report));
|
||||
setReports(map);
|
||||
});
|
||||
|
||||
return () => { unsubEmp(); unsubSub(); unsubRep(); };
|
||||
}, [orgId]);
|
||||
|
||||
const upsertOrg = async (data: Partial<OrgData>) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
const updatedOrg = { ...(org || { orgId, name: 'Demo Company' }), ...data } as OrgData;
|
||||
setOrg(updatedOrg);
|
||||
|
||||
// Also sync with server for multi-tenant persistence
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/organizations/${orgId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to sync organization data with server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to sync organization data:', error);
|
||||
}
|
||||
} else {
|
||||
// Firebase mode - save to Firestore
|
||||
const orgRef = doc(db, 'orgs', orgId);
|
||||
await setDoc(orgRef, data, { merge: true });
|
||||
|
||||
// Update local state
|
||||
const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
|
||||
setOrg(updatedOrg);
|
||||
}
|
||||
};
|
||||
|
||||
const updateOrg = async (data: Partial<OrgData>) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
const updatedOrg = { ...(org || { orgId, name: 'Demo Company' }), ...data } as OrgData;
|
||||
setOrg(updatedOrg);
|
||||
|
||||
// Also sync with server for multi-tenant persistence
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/organizations/${orgId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to sync organization data with server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to sync organization data:', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const orgRef = doc(db, 'orgs', orgId);
|
||||
await setDoc(orgRef, data, { merge: true });
|
||||
};
|
||||
|
||||
const saveReport = async (employeeId: string, report: Report) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
setReports(prev => ({ ...prev, [employeeId]: report }));
|
||||
// Persist to localStorage
|
||||
demoStorage.saveEmployeeReport(orgId, employeeId, report);
|
||||
return;
|
||||
}
|
||||
const ref = doc(db, 'orgs', orgId, 'reports', employeeId);
|
||||
await setDoc(ref, report, { merge: true });
|
||||
};
|
||||
|
||||
const inviteEmployee = async ({ name, email }: { name: string; email: string }) => {
|
||||
// Always use Cloud Functions for invites to ensure multi-tenant compliance
|
||||
const response = await fetch(`${API_URL}/createInvitation`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, orgId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create invite: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { code, employee, inviteLink } = data;
|
||||
|
||||
// Store employee locally for immediate UI update
|
||||
if (!isFirebaseConfigured) {
|
||||
const newEmployee = { ...employee, orgId };
|
||||
setEmployees(prev => {
|
||||
if (prev.find(e => e.id === employee.id)) return prev;
|
||||
return [...prev, newEmployee];
|
||||
});
|
||||
demoStorage.saveEmployee(newEmployee);
|
||||
} else {
|
||||
// For Firebase, the employee will be created when they accept the invite
|
||||
// But we can add them to local state for immediate UI update
|
||||
const newEmployee = { ...employee, orgId };
|
||||
setEmployees(prev => {
|
||||
if (prev.find(e => e.id === employee.id)) return prev;
|
||||
return [...prev, newEmployee];
|
||||
});
|
||||
}
|
||||
|
||||
return { employeeId: employee.id, inviteLink };
|
||||
};
|
||||
|
||||
const getReportVersions = async (employeeId: string) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
return reportVersions[employeeId] || [];
|
||||
}
|
||||
const col = collection(db, 'orgs', orgId, 'reports', employeeId, 'versions');
|
||||
const snap = await getDocs(col);
|
||||
const arr: Array<{ id: string; createdAt: number; report: Report }> = [];
|
||||
snap.forEach(d => {
|
||||
const data = d.data() as any;
|
||||
arr.push({ id: d.id, createdAt: data.createdAt ?? 0, report: data.report as Report });
|
||||
});
|
||||
return arr.sort((a, b) => b.createdAt - a.createdAt);
|
||||
};
|
||||
|
||||
const saveReportVersion = async (employeeId: string, report: Report) => {
|
||||
const version = { id: Date.now().toString(), createdAt: Date.now(), report };
|
||||
if (!isFirebaseConfigured) {
|
||||
setReportVersions(prev => ({ ...prev, [employeeId]: [version, ...(prev[employeeId] || [])] }));
|
||||
return;
|
||||
}
|
||||
const ref = doc(db, 'orgs', orgId, 'reports', employeeId, 'versions', version.id);
|
||||
await setDoc(ref, { createdAt: version.createdAt, report });
|
||||
};
|
||||
|
||||
const acceptInvite = async (code: string) => {
|
||||
if (!code) return;
|
||||
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: mark invite as used
|
||||
demoStorage.markInviteUsed(code);
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteRef = doc(db, 'orgs', orgId, 'invites', code);
|
||||
const snap = await getDoc(inviteRef);
|
||||
if (!snap.exists()) return;
|
||||
const data = snap.data() as any;
|
||||
// Minimal: mark accepted
|
||||
await setDoc(inviteRef, { ...data, acceptedAt: Date.now() }, { merge: true });
|
||||
};
|
||||
|
||||
const saveCompanyReport = async (summary: string) => {
|
||||
const id = Date.now().toString();
|
||||
const createdAt = Date.now();
|
||||
if (!isFirebaseConfigured) {
|
||||
const reportData = { id, createdAt, summary };
|
||||
setCompanyReports(prev => [reportData, ...prev]);
|
||||
// Persist to localStorage (note: this method stores simple reports)
|
||||
return;
|
||||
}
|
||||
const ref = doc(db, 'orgs', orgId, 'companyReports', id);
|
||||
await setDoc(ref, { createdAt, summary });
|
||||
};
|
||||
|
||||
const getCompanyReportHistory = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
return companyReports;
|
||||
}
|
||||
const col = collection(db, 'orgs', orgId, 'companyReports');
|
||||
const snap = await getDocs(col);
|
||||
const arr: Array<{ id: string; createdAt: number; summary: string }> = [];
|
||||
snap.forEach(d => {
|
||||
const data = d.data() as any;
|
||||
arr.push({ id: d.id, createdAt: data.createdAt ?? 0, summary: data.summary ?? '' });
|
||||
});
|
||||
return arr.sort((a, b) => b.createdAt - a.createdAt);
|
||||
};
|
||||
|
||||
const seedInitialData = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Start with empty employee list for clean demo experience
|
||||
setEmployees([]);
|
||||
setSubmissions({ [SUBMISSIONS_DATA.employeeId]: SUBMISSIONS_DATA });
|
||||
setReports({ [REPORT_DATA.employeeId]: REPORT_DATA });
|
||||
setFullCompanyReports([SAMPLE_COMPANY_REPORT]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with clean slate - let users invite their own employees
|
||||
// (Removed automatic seeding per user feedback)
|
||||
};
|
||||
|
||||
const saveFullCompanyReport = async (report: CompanyReport) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
setFullCompanyReports(prev => [report, ...prev]);
|
||||
// Persist to localStorage
|
||||
demoStorage.saveCompanyReport(orgId, report);
|
||||
return;
|
||||
}
|
||||
const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id);
|
||||
await setDoc(ref, report);
|
||||
};
|
||||
|
||||
const getFullCompanyReportHistory = async (): Promise<CompanyReport[]> => {
|
||||
if (!isFirebaseConfigured) {
|
||||
return fullCompanyReports;
|
||||
}
|
||||
const col = collection(db, 'orgs', orgId, 'fullCompanyReports');
|
||||
const snap = await getDocs(col);
|
||||
const arr: CompanyReport[] = [];
|
||||
snap.forEach(d => {
|
||||
arr.push({ id: d.id, ...d.data() } as CompanyReport);
|
||||
});
|
||||
return arr.sort((a, b) => b.createdAt - a.createdAt);
|
||||
};
|
||||
|
||||
const generateCompanyReport = async (): Promise<CompanyReport> => {
|
||||
// Generate comprehensive company report based on current data
|
||||
const totalEmployees = employees.length;
|
||||
const submittedEmployees = Object.keys(submissions).length;
|
||||
const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
|
||||
|
||||
// Department breakdown
|
||||
const deptMap = new Map<string, number>();
|
||||
employees.forEach(emp => {
|
||||
const dept = emp.department || 'Unassigned';
|
||||
deptMap.set(dept, (deptMap.get(dept) || 0) + 1);
|
||||
});
|
||||
const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count }));
|
||||
|
||||
// Analyze employee reports for insights
|
||||
const reportValues = Object.values(reports) as Report[];
|
||||
const organizationalStrengths: string[] = [];
|
||||
const organizationalRisks: string[] = [];
|
||||
|
||||
reportValues.forEach(report => {
|
||||
if (report.strengths) {
|
||||
organizationalStrengths.push(...report.strengths);
|
||||
}
|
||||
if (report.risks) {
|
||||
organizationalRisks.push(...report.risks);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove duplicates and take top items
|
||||
const uniqueStrengths = [...new Set(organizationalStrengths)].slice(0, 5);
|
||||
const uniqueRisks = [...new Set(organizationalRisks)].slice(0, 5);
|
||||
|
||||
const gradingBreakdown = [
|
||||
{ category: 'Execution', value: 70 + Math.random() * 15 },
|
||||
{ category: 'People', value: 70 + Math.random() * 15 },
|
||||
{ category: 'Strategy', value: 65 + Math.random() * 15 },
|
||||
{ category: 'Risk', value: 60 + Math.random() * 15 }
|
||||
];
|
||||
const legacy = gradingBreakdown.reduce<Record<string, number>>((acc, g) => { acc[g.category.toLowerCase()] = Math.round((g.value / 100) * 5 * 10) / 10; return acc; }, {});
|
||||
const report: CompanyReport = {
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
overview: {
|
||||
totalEmployees,
|
||||
departmentBreakdown,
|
||||
submissionRate,
|
||||
lastUpdated: Date.now(),
|
||||
averagePerformanceScore: gradingBreakdown.reduce((a, g) => a + g.value, 0) / gradingBreakdown.length / 20,
|
||||
riskLevel: uniqueRisks.length > 4 ? 'High' : uniqueRisks.length > 2 ? 'Medium' : 'Low'
|
||||
},
|
||||
personnelChanges: { newHires: [], promotions: [], departures: [] },
|
||||
immediateHiringNeeds: [],
|
||||
operatingPlan: {
|
||||
nextQuarterGoals: ['Increase productivity', 'Implement review system'],
|
||||
keyInitiatives: ['Mentorship program'],
|
||||
resourceNeeds: ['Senior engineer'],
|
||||
riskMitigation: ['Cross-training']
|
||||
},
|
||||
forwardOperatingPlan: { // legacy fields
|
||||
quarterlyGoals: ['Increase productivity'],
|
||||
resourceNeeds: ['Senior engineer'],
|
||||
riskMitigation: ['Cross-training']
|
||||
},
|
||||
organizationalStrengths: uniqueStrengths.map(s => ({ area: s, description: s })),
|
||||
organizationalRisks: uniqueRisks,
|
||||
organizationalImpactSummary: 'Impact summary placeholder',
|
||||
gradingBreakdown,
|
||||
gradingOverview: legacy,
|
||||
executiveSummary: `Company overview for ${org?.name || 'Organization'} as of ${new Date().toLocaleDateString()}. Total workforce: ${totalEmployees}. Submission rate: ${submissionRate.toFixed(1)}%. Key strengths: ${uniqueStrengths.slice(0, 2).join(', ')}. Risks: ${uniqueRisks.slice(0, 2).join(', ')}.`
|
||||
};
|
||||
|
||||
await saveFullCompanyReport(report);
|
||||
return report;
|
||||
};
|
||||
|
||||
const generateCompanyWiki = async (orgOverride?: OrgData): Promise<CompanyReport> => {
|
||||
const orgData = orgOverride || org;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/generateCompanyWiki`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ org: orgData, submissions })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to generate company wiki');
|
||||
const payload = await res.json();
|
||||
const data: CompanyReport = payload.report || payload; // backward compatibility
|
||||
await saveFullCompanyReport(data);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('generateCompanyWiki error, falling back to local synthetic:', e);
|
||||
return generateCompanyReport();
|
||||
}
|
||||
};
|
||||
|
||||
const isOwner = (employeeId?: string): boolean => {
|
||||
const currentEmployee = employeeId ? employees.find(e => e.id === employeeId) :
|
||||
employees.find(e => e.email === user?.email);
|
||||
return currentEmployee?.isOwner === true;
|
||||
};
|
||||
|
||||
const getEmployeeReport = async (employeeId: string) => {
|
||||
try {
|
||||
if (isFirebaseConfigured && user) {
|
||||
// Firebase implementation
|
||||
const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId));
|
||||
if (reportDoc.exists()) {
|
||||
return { success: true, report: reportDoc.data() };
|
||||
}
|
||||
return { success: false, error: 'Report not found' };
|
||||
} else {
|
||||
// Demo mode - call API
|
||||
const response = await fetch(`${API_URL}/api/employee-report/${employeeId}`);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching employee report:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const getEmployeeReports = async () => {
|
||||
try {
|
||||
if (isFirebaseConfigured && user) {
|
||||
// Firebase implementation
|
||||
const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports'));
|
||||
const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
||||
return { success: true, reports };
|
||||
} else {
|
||||
// Demo mode - call API
|
||||
const response = await fetch(`${API_URL}/api/employee-reports`);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching employee reports:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const value = {
|
||||
org,
|
||||
orgId,
|
||||
employees,
|
||||
submissions,
|
||||
reports,
|
||||
upsertOrg,
|
||||
saveReport,
|
||||
inviteEmployee,
|
||||
getReportVersions,
|
||||
saveReportVersion,
|
||||
acceptInvite,
|
||||
saveCompanyReport,
|
||||
getCompanyReportHistory,
|
||||
saveFullCompanyReport,
|
||||
getFullCompanyReportHistory,
|
||||
generateCompanyReport,
|
||||
generateCompanyWiki,
|
||||
seedInitialData,
|
||||
isOwner,
|
||||
issueInviteViaApi: async ({ name, email, role, department }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/createInvitation`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, role, department, orgId })
|
||||
});
|
||||
if (!res.ok) throw new Error('invite creation failed');
|
||||
const json = await res.json();
|
||||
// Optimistically add employee shell (not yet active until consume)
|
||||
setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, { ...json.employee }]);
|
||||
return json;
|
||||
} catch (e) {
|
||||
console.error('issueInviteViaApi error', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
getInviteStatus: async (code: string) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: check localStorage first, then server
|
||||
const invite = demoStorage.getInvite(code);
|
||||
if (invite) {
|
||||
return { used: invite.used, employee: invite.employee };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
console.error('getInviteStatus error', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
consumeInvite: async (code: string) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: mark invite as used in localStorage and update state
|
||||
const invite = demoStorage.getInvite(code);
|
||||
if (invite && !invite.used) {
|
||||
demoStorage.markInviteUsed(code);
|
||||
// Ensure employee is in the list with proper typing
|
||||
const convertedEmployee: Employee = {
|
||||
id: invite.employee.id,
|
||||
name: invite.employee.name,
|
||||
email: invite.employee.email,
|
||||
initials: invite.employee.name ? invite.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : invite.employee.email.substring(0, 2).toUpperCase(),
|
||||
department: invite.employee.department,
|
||||
role: invite.employee.role
|
||||
};
|
||||
setEmployees(prev => prev.find(e => e.id === invite.employee.id) ? prev : [...prev, convertedEmployee]);
|
||||
return { employee: convertedEmployee };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/consumeInvitation?code=${code}`, { method: 'POST' });
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json();
|
||||
// Mark employee as active (could set a flag later)
|
||||
setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, json.employee]);
|
||||
return json;
|
||||
} catch (e) {
|
||||
console.error('consumeInvite error', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode: save to localStorage and call server endpoint
|
||||
try {
|
||||
const submission = {
|
||||
employeeId,
|
||||
orgId,
|
||||
answers,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
// Save to localStorage for persistence
|
||||
demoStorage.saveSubmission(submission);
|
||||
|
||||
// Also call Cloud Function for processing with orgId
|
||||
const employee = employees.find(e => e.id === employeeId);
|
||||
const res = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
employeeId,
|
||||
answers,
|
||||
orgId,
|
||||
employee
|
||||
})
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to submit to server');
|
||||
|
||||
// Update local state for UI with proper typing
|
||||
const convertedSubmission: Submission = {
|
||||
employeeId,
|
||||
answers: Object.entries(answers).map(([question, answer]) => ({
|
||||
question,
|
||||
answer
|
||||
}))
|
||||
};
|
||||
setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('submitEmployeeAnswers error', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Firebase mode: save to Firestore
|
||||
try {
|
||||
const ref = doc(db, 'orgs', orgId, 'submissions', employeeId);
|
||||
await setDoc(ref, { ...answers, createdAt: Date.now() }, { merge: true });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('submitEmployeeAnswers error', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
generateEmployeeReport: async (employee: Employee) => {
|
||||
try {
|
||||
const submission = submissions[employee.id]?.answers || submissions[employee.id] || {};
|
||||
const res = await fetch(`${API_URL}/generateEmployeeReport`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ employee, submission })
|
||||
});
|
||||
if (!res.ok) throw new Error('failed to generate');
|
||||
const json = await res.json();
|
||||
if (json.report) {
|
||||
setReports(prev => ({ ...prev, [employee.id]: json.report }));
|
||||
return json.report as Report;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('generateEmployeeReport error', e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getEmployeeReport: async (employeeId: string) => {
|
||||
try {
|
||||
if (isFirebaseConfigured && user) {
|
||||
// Firebase implementation
|
||||
const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId));
|
||||
if (reportDoc.exists()) {
|
||||
return { success: true, report: reportDoc.data() };
|
||||
}
|
||||
return { success: false, error: 'Report not found' };
|
||||
} else {
|
||||
// Demo mode - call Cloud Function
|
||||
const response = await fetch(`${API_URL}/generateEmployeeReport?employeeId=${employeeId}`);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching employee report:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
getEmployeeReports: async () => {
|
||||
try {
|
||||
if (isFirebaseConfigured && user) {
|
||||
// Firebase implementation
|
||||
const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports'));
|
||||
const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
||||
return { success: true, reports };
|
||||
} else {
|
||||
// Demo mode - call Cloud Function
|
||||
const response = await fetch(`${API_URL}/generateEmployeeReport`);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching employee reports:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<OrgContext.Provider value={value}>
|
||||
{children}
|
||||
</OrgContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useOrg = () => {
|
||||
const ctx = useContext(OrgContext);
|
||||
if (!ctx) throw new Error('useOrg must be used within OrgProvider');
|
||||
return ctx;
|
||||
};
|
||||
53
contexts/ThemeContext.tsx
Normal file
53
contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
|
||||
import { Theme } from '../types';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
try {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
return (storedTheme as Theme) || Theme.System;
|
||||
} catch (error) {
|
||||
console.warn('Could not access localStorage. Defaulting to system theme.', error);
|
||||
return Theme.System;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
const systemIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (theme === Theme.System) {
|
||||
root.classList.toggle('dark', systemIsDark);
|
||||
} else {
|
||||
root.classList.toggle('dark', theme === Theme.Dark);
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('theme', theme);
|
||||
} catch (error) {
|
||||
console.warn(`Could not save theme to localStorage: ${error}`);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
300
contexts/UserOrganizationsContext.tsx
Normal file
300
contexts/UserOrganizationsContext.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { isFirebaseConfigured } from '../services/firebase';
|
||||
import { API_URL } from '../constants';
|
||||
import { demoStorage } from '../services/demoStorage';
|
||||
|
||||
interface UserOrganization {
|
||||
orgId: string;
|
||||
name: string;
|
||||
role: 'owner' | 'admin' | 'employee';
|
||||
onboardingCompleted: boolean;
|
||||
joinedAt: number;
|
||||
}
|
||||
|
||||
interface UserOrganizationsContextType {
|
||||
organizations: UserOrganization[];
|
||||
selectedOrgId: string | null;
|
||||
loading: boolean;
|
||||
selectOrganization: (orgId: string) => void;
|
||||
createOrganization: (name: string) => Promise<{ orgId: string; requiresSubscription?: boolean }>;
|
||||
joinOrganization: (inviteCode: string) => Promise<string>;
|
||||
refreshOrganizations: () => Promise<void>;
|
||||
createCheckoutSession: (orgId: string, userEmail: string) => Promise<{ sessionUrl: string; sessionId: string }>;
|
||||
getSubscriptionStatus: (orgId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const UserOrganizationsContext = createContext<UserOrganizationsContextType | undefined>(undefined);
|
||||
|
||||
export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user } = useAuth();
|
||||
const [organizations, setOrganizations] = useState<UserOrganization[]>([]);
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Load user's organizations
|
||||
const loadOrganizations = async () => {
|
||||
if (!user) {
|
||||
setOrganizations([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode - fetch from server API
|
||||
const response = await fetch(`${API_URL}/api/user/${user.uid}/organizations`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrganizations(data.organizations || []);
|
||||
} else {
|
||||
console.error('Failed to load organizations:', response.status);
|
||||
setOrganizations([]);
|
||||
}
|
||||
} else {
|
||||
// Firebase mode - fetch from Cloud Functions
|
||||
const response = await fetch(`${API_URL}/getUserOrganizations?userId=${user.uid}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrganizations(data.organizations || []);
|
||||
} else {
|
||||
console.error('Failed to load organizations:', response.status);
|
||||
setOrganizations([]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load organizations:', error);
|
||||
setOrganizations([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize selected org from session storage
|
||||
useEffect(() => {
|
||||
const savedOrgId = sessionStorage.getItem('auditly_selected_org');
|
||||
if (savedOrgId) {
|
||||
setSelectedOrgId(savedOrgId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load organizations when user changes
|
||||
useEffect(() => {
|
||||
loadOrganizations();
|
||||
}, [user]);
|
||||
|
||||
const selectOrganization = (orgId: string) => {
|
||||
setSelectedOrgId(orgId);
|
||||
sessionStorage.setItem('auditly_selected_org', orgId);
|
||||
};
|
||||
|
||||
const createOrganization = async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => {
|
||||
if (!user) throw new Error('User not authenticated');
|
||||
|
||||
try {
|
||||
let newOrg: UserOrganization;
|
||||
let requiresSubscription = false;
|
||||
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode - use server API
|
||||
const response = await fetch(`${API_URL}/api/organizations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, userId: user.uid })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create organization: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
newOrg = {
|
||||
orgId: data.orgId,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
onboardingCompleted: data.onboardingCompleted,
|
||||
joinedAt: data.joinedAt
|
||||
};
|
||||
|
||||
setOrganizations(prev => [...prev, newOrg]);
|
||||
} else {
|
||||
// Firebase mode - use Cloud Function
|
||||
const response = await fetch(`${API_URL}/createOrganization`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, userId: user.uid })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create organization: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
newOrg = {
|
||||
orgId: data.orgId,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
onboardingCompleted: data.onboardingCompleted,
|
||||
joinedAt: data.joinedAt
|
||||
};
|
||||
|
||||
requiresSubscription = data.requiresSubscription || false;
|
||||
setOrganizations(prev => [...prev, newOrg]);
|
||||
}
|
||||
|
||||
return { orgId: newOrg.orgId, requiresSubscription };
|
||||
} catch (error) {
|
||||
console.error('Failed to create organization:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const joinOrganization = async (inviteCode: string): Promise<string> => {
|
||||
if (!user) throw new Error('User not authenticated');
|
||||
|
||||
try {
|
||||
if (!isFirebaseConfigured) {
|
||||
// Demo mode - use server API to get and consume invite
|
||||
const inviteStatusRes = await fetch(`/api/invitations/${inviteCode}`);
|
||||
if (!inviteStatusRes.ok) {
|
||||
throw new Error('Invalid or expired invite code');
|
||||
}
|
||||
|
||||
const inviteData = await inviteStatusRes.json();
|
||||
if (inviteData.used) {
|
||||
throw new Error('Invite code has already been used');
|
||||
}
|
||||
|
||||
// Consume the invite
|
||||
const consumeRes = await fetch(`/api/invitations/${inviteCode}/consume`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!consumeRes.ok) {
|
||||
throw new Error('Failed to consume invite');
|
||||
}
|
||||
|
||||
const consumedData = await consumeRes.json();
|
||||
const orgId = consumedData.orgId;
|
||||
|
||||
// Get organization data (this might be from localStorage for demo mode)
|
||||
const orgData = demoStorage.getOrganization(orgId);
|
||||
if (!orgData) {
|
||||
throw new Error('Organization not found');
|
||||
}
|
||||
|
||||
const userOrg: UserOrganization = {
|
||||
orgId: orgId,
|
||||
name: orgData.name,
|
||||
role: 'employee',
|
||||
onboardingCompleted: orgData.onboardingCompleted || false,
|
||||
joinedAt: Date.now()
|
||||
};
|
||||
|
||||
setOrganizations(prev => [...prev, userOrg]);
|
||||
return orgId;
|
||||
} else {
|
||||
// Firebase mode - use Cloud Function
|
||||
const response = await fetch(`${API_URL}/joinOrganization`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: user.uid, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to join organization');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const userOrg: UserOrganization = {
|
||||
orgId: data.orgId,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
onboardingCompleted: data.onboardingCompleted,
|
||||
joinedAt: data.joinedAt
|
||||
};
|
||||
|
||||
setOrganizations(prev => [...prev, userOrg]);
|
||||
return data.orgId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to join organization:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshOrganizations = async () => {
|
||||
setLoading(true);
|
||||
await loadOrganizations();
|
||||
};
|
||||
|
||||
const createCheckoutSession = async (orgId: string, userEmail: string): Promise<{ sessionUrl: string; sessionId: string }> => {
|
||||
if (!user) throw new Error('User not authenticated');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/createCheckoutSession`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
orgId,
|
||||
userId: user.uid,
|
||||
userEmail
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to create checkout session');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
sessionUrl: data.sessionUrl,
|
||||
sessionId: data.sessionId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create checkout session:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getSubscriptionStatus = async (orgId: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/getSubscriptionStatus?orgId=${orgId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get subscription status');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get subscription status:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UserOrganizationsContext.Provider value={{
|
||||
organizations,
|
||||
selectedOrgId,
|
||||
loading,
|
||||
selectOrganization,
|
||||
createOrganization,
|
||||
joinOrganization,
|
||||
refreshOrganizations,
|
||||
createCheckoutSession,
|
||||
getSubscriptionStatus
|
||||
}}>
|
||||
{children}
|
||||
</UserOrganizationsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUserOrganizations = () => {
|
||||
const context = useContext(UserOrganizationsContext);
|
||||
if (!context) {
|
||||
throw new Error('useUserOrganizations must be used within UserOrganizationsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user