Created
May 15, 2025 11:23
-
-
Save Biggiegoesallthewayup/aa1171230463701051e102540fcc5ae0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AuthSession, AuthUser } from '@supabase/supabase-js/dist/module'; | |
// Define custom types that match Supabase's structure but with our required fields | |
export type User = { | |
id: string; | |
aud: string; | |
role: string; | |
email?: string; | |
app_metadata: any; | |
user_metadata: any; | |
created_at: string; | |
updated_at: string; | |
}; | |
export type Session = { | |
access_token: string; | |
refresh_token: string; | |
expires_at: number; | |
expires_in: number; | |
user: User; | |
}; | |
// Define the UserProfile type to match our database schema | |
export interface UserProfile { | |
id: string; | |
first_name: string; | |
last_name: string; | |
role: 'individual_agent' | 'office_manager' | 'agent'; | |
avatar_url: string | null; | |
phone: string | null; | |
bio: string | null; | |
} | |
// Define the Office type to match our database schema | |
export interface Office { | |
id: string; | |
name: string; | |
address: string | null; | |
logo_url: string | null; | |
phone: string | null; | |
email: string | null; | |
website: string | null; | |
} | |
export type AuthContextType = { | |
user: User | null; | |
session: Session | null; | |
profile: UserProfile | null; | |
office: Office | null; | |
isLoading: boolean; | |
signIn: (email: string, password: string) => Promise<{ | |
error: Error | null; | |
success: boolean; | |
}>; | |
signUp: ( | |
email: string, | |
password: string, | |
userData?: { | |
first_name?: string; | |
last_name?: string; | |
role?: 'individual_agent' | 'office_manager' | 'agent'; | |
} | |
) => Promise<{ | |
error: Error | null; | |
success: boolean; | |
}>; | |
signOut: () => Promise<void>; | |
refreshUserProfile: () => Promise<void>; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Link } from "react-router-dom"; | |
import { Button } from "@/components/ui/button"; | |
import { Input } from "@/components/ui/input"; | |
import { Label } from "@/components/ui/label"; | |
import { LoginFormProps } from "./types"; | |
export const LoginForm = ({ | |
email, | |
setEmail, | |
password, | |
setPassword, | |
handleLogin, | |
isSubmitting | |
}: LoginFormProps) => { | |
return ( | |
<form onSubmit={handleLogin}> | |
<div className="space-y-4"> | |
<div className="space-y-2"> | |
<Label htmlFor="email">Email cím</Label> | |
<Input | |
id="email" | |
type="email" | |
placeholder="pelda@email.hu" | |
value={email} | |
onChange={(e) => setEmail(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="space-y-2"> | |
<div className="flex items-center justify-between"> | |
<Label htmlFor="password">Jelszó</Label> | |
<Link | |
to="/forgot-password" | |
className="text-sm text-blue-600 hover:text-blue-800" | |
> | |
Elfelejtett jelszó? | |
</Link> | |
</div> | |
<Input | |
id="password" | |
type="password" | |
placeholder="••••••••" | |
value={password} | |
onChange={(e) => setPassword(e.target.value)} | |
required | |
/> | |
</div> | |
</div> | |
<div className="mt-6 flex flex-col space-y-4"> | |
<Button | |
type="submit" | |
className="w-full" | |
disabled={isSubmitting} | |
> | |
{isSubmitting ? "Bejelentkezés..." : "Bejelentkezés"} | |
</Button> | |
<div className="text-center text-sm"> | |
Nincs még fiókod?{" "} | |
<Link | |
to="/signup" | |
className="text-blue-600 hover:text-blue-800 font-medium" | |
> | |
Regisztrálj itt | |
</Link> | |
</div> | |
</div> | |
</form> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect } from 'react'; | |
import { useAuth } from '@/contexts/AuthContext'; | |
import { TeamMembersList } from './TeamMembersList'; | |
import { NoOfficeMessage } from './NoOfficeMessage'; | |
import { fetchOfficeMembersWithProfiles, OfficeWithMembers } from './officeUtils'; | |
import { Skeleton } from '@/components/ui/skeleton'; | |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; | |
import { AlertCircle, RefreshCcw } from 'lucide-react'; | |
import { Button } from '@/components/ui/button'; | |
export const TeamManagement = () => { | |
const { user, profile, office, refreshUserProfile } = useAuth(); | |
const [officeWithMembers, setOfficeWithMembers] = useState<OfficeWithMembers | null>(null); | |
const [isLoading, setIsLoading] = useState(true); | |
const [error, setError] = useState<string | null>(null); | |
const loadOfficeMembers = async () => { | |
if (!office) { | |
setIsLoading(false); | |
return; | |
} | |
try { | |
setIsLoading(true); | |
setError(null); | |
console.log("TeamManagement: Loading office members for office:", office.id); | |
const officeMembers = await fetchOfficeMembersWithProfiles(office.id); | |
if (!officeMembers) { | |
setError("Nem sikerült betölteni az iroda tagjait. Kérjük próbálja újra később."); | |
return; | |
} | |
console.log("TeamManagement: Loaded office members:", officeMembers.members?.length || 0); | |
setOfficeWithMembers(officeMembers); | |
} catch (error) { | |
console.error("Error loading office members:", error); | |
setError("Váratlan hiba történt az iroda tagjainak betöltése közben."); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
useEffect(() => { | |
if (office) { | |
console.log("TeamManagement: Office available, loading members"); | |
loadOfficeMembers(); | |
} else { | |
console.log("TeamManagement: No office available"); | |
setIsLoading(false); | |
} | |
}, [office]); | |
if (isLoading) { | |
return <LoadingState />; | |
} | |
if (!office) { | |
return <NoOfficeMessage profile={profile} onRefreshProfile={refreshUserProfile} />; | |
} | |
const isOfficeManager = profile?.role === 'office_manager'; | |
if (error) { | |
return ( | |
<Alert variant="destructive" className="mb-6"> | |
<AlertCircle className="h-4 w-4" /> | |
<AlertTitle>Hiba</AlertTitle> | |
<AlertDescription className="flex flex-col gap-3"> | |
<p>{error}</p> | |
<Button | |
variant="outline" | |
size="sm" | |
className="w-fit flex items-center gap-2" | |
onClick={loadOfficeMembers} | |
> | |
<RefreshCcw className="h-4 w-4" /> Újrapróbálkozás | |
</Button> | |
</AlertDescription> | |
</Alert> | |
); | |
} | |
return ( | |
<div className="space-y-6"> | |
{officeWithMembers ? ( | |
<TeamMembersList | |
officeWithMembers={officeWithMembers} | |
isOfficeManager={isOfficeManager} | |
onMemberChange={loadOfficeMembers} | |
/> | |
) : ( | |
<Alert variant="destructive" className="mb-6"> | |
<AlertCircle className="h-4 w-4" /> | |
<AlertTitle>Hiba</AlertTitle> | |
<AlertDescription className="flex flex-col gap-3"> | |
<p>Nem sikerült betölteni az iroda tagjait</p> | |
<Button | |
variant="outline" | |
size="sm" | |
className="w-fit flex items-center gap-2" | |
onClick={loadOfficeMembers} | |
> | |
<RefreshCcw className="h-4 w-4" /> Újrapróbálkozás | |
</Button> | |
</AlertDescription> | |
</Alert> | |
)} | |
</div> | |
); | |
}; | |
const LoadingState = () => ( | |
<div className="space-y-6"> | |
<div className="flex justify-between items-center"> | |
<Skeleton className="h-8 w-1/3" /> | |
<Skeleton className="h-9 w-32" /> | |
</div> | |
<div className="space-y-4"> | |
{[1, 2, 3].map((i) => ( | |
<Skeleton key={i} className="h-20 w-full" /> | |
))} | |
</div> | |
</div> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { supabase } from "@/integrations/supabase/client"; | |
import { User, UserProfile, Office, Session } from '@/types/auth'; | |
import { AuthSession, AuthUser } from '@supabase/supabase-js/dist/module'; | |
// Function to convert Supabase User to our Custom User type | |
export const convertUser = (supabaseUser: AuthUser): User => { | |
return { | |
id: supabaseUser.id, | |
aud: supabaseUser.aud || '', | |
role: supabaseUser.role || '', | |
email: supabaseUser.email, | |
app_metadata: supabaseUser.app_metadata || {}, | |
user_metadata: supabaseUser.user_metadata || {}, | |
created_at: supabaseUser.created_at || '', | |
updated_at: supabaseUser.updated_at || '' | |
}; | |
}; | |
// Function to convert Supabase Session to our Custom Session type | |
export const convertSession = (supabaseSession: AuthSession): Session => { | |
return { | |
access_token: supabaseSession.access_token || '', | |
refresh_token: supabaseSession.refresh_token || '', | |
expires_at: supabaseSession.expires_at || 0, | |
expires_in: supabaseSession.expires_in || 0, | |
user: convertUser(supabaseSession.user) | |
}; | |
}; | |
// Function to fetch the user's profile data | |
export const fetchUserProfile = async (userId: string): Promise<UserProfile | null> => { | |
try { | |
console.log("AuthProvider: Fetching user profile for", userId); | |
const { data, error } = await supabase | |
.from('profiles') | |
.select('*') | |
.eq('id', userId) | |
.single(); | |
if (error) { | |
console.error("Error fetching user profile:", error); | |
// Check if this is a "not found" error, which might indicate we need to create the profile | |
if (error.code === 'PGRST116') { // No rows returned | |
console.log("Profile not found, attempting to create one based on auth metadata"); | |
const { data: userData } = await supabase.auth.getUser(); | |
const user = userData?.user; | |
if (user && user.user_metadata) { | |
const newProfile = { | |
id: userId, | |
first_name: user.user_metadata.first_name || '', | |
last_name: user.user_metadata.last_name || '', | |
role: user.user_metadata.role || 'individual_agent', | |
avatar_url: null, | |
phone: null, | |
bio: null | |
}; | |
const { error: insertError } = await supabase | |
.from('profiles') | |
.insert(newProfile); | |
if (insertError) { | |
console.error("Failed to create profile:", insertError); | |
return null; | |
} | |
console.log("Successfully created profile from auth metadata", newProfile); | |
return newProfile as UserProfile; | |
} | |
} | |
return null; | |
} | |
// If the role in auth metadata is office_manager but profile shows different, update it | |
const { data: userData } = await supabase.auth.getUser(); | |
const user = userData?.user; | |
if (user && user.user_metadata && user.user_metadata.role === 'office_manager' && data.role !== 'office_manager') { | |
console.log("Detected mismatch between auth metadata role and profile role. Updating profile..."); | |
const { error: updateError } = await supabase | |
.from('profiles') | |
.update({ role: 'office_manager' }) | |
.eq('id', userId); | |
if (updateError) { | |
console.error("Failed to update profile role:", updateError); | |
} else { | |
data.role = 'office_manager'; | |
console.log("Updated profile role to office_manager"); | |
} | |
} | |
console.log("AuthProvider: Retrieved profile", data); | |
return data as UserProfile; | |
} catch (error) { | |
console.error("Unexpected error fetching profile:", error); | |
return null; | |
} | |
}; | |
// Function to fetch the user's office data if they belong to one | |
export const fetchUserOffice = async (userId: string): Promise<Office | null> => { | |
try { | |
console.log("AuthProvider: Checking if user belongs to an office"); | |
// First get the office membership - don't use table prefixes in the select clause | |
const { data: memberData, error: memberError } = await supabase | |
.from('office_members') | |
.select('office_id,role') | |
.eq('user_id', userId) | |
.single(); | |
if (memberError) { | |
if (memberError.code !== 'PGRST116') { // PGRST116 is "no rows returned" error | |
console.error("Error fetching office membership:", memberError); | |
} else { | |
console.log("User does not belong to any office"); | |
// If user is an office_manager in their profile but not part of any office, | |
// we should create an office and add them to it | |
const userProfile = await fetchUserProfile(userId); | |
if (userProfile && userProfile.role === 'office_manager') { | |
console.log("User is an office_manager but has no office. Creating one..."); | |
const officeName = `${userProfile.first_name}'s Office`; | |
// Check if user already has an office first by name to avoid duplicates | |
const { data: existingOffices, error: existingOfficeError } = await supabase | |
.from('offices') | |
.select('id') | |
.eq('name', officeName) | |
.eq('created_by', userId); | |
if (existingOfficeError) { | |
console.error("Error checking existing offices:", existingOfficeError); | |
return null; | |
} | |
if (existingOffices && existingOffices.length > 0) { | |
console.log("User already has an office with this name:", existingOffices[0]); | |
// Check if user is already a member of this office | |
const { data: existingMembership, error: membershipCheckError } = await supabase | |
.from('office_members') | |
.select('id') | |
.eq('office_id', existingOffices[0].id) | |
.eq('user_id', userId); | |
if (membershipCheckError) { | |
console.error("Error checking membership:", membershipCheckError); | |
// Continue to attempt to add the user anyway | |
} | |
if (!existingMembership || existingMembership.length === 0) { | |
// Add the user as office manager if not already a member | |
const { error: membershipError } = await supabase | |
.from('office_members') | |
.insert({ | |
user_id: userId, | |
office_id: existingOffices[0].id, | |
role: 'office_manager', | |
status: 'active', | |
joined_at: new Date().toISOString() | |
}); | |
if (membershipError) { | |
console.error("Failed to add user as office manager:", membershipError); | |
} else { | |
console.log("Added user as office manager"); | |
} | |
} | |
// Then get the office details | |
const { data: officeData, error: officeQueryError } = await supabase | |
.from('offices') | |
.select('*') | |
.eq('id', existingOffices[0].id) | |
.single(); | |
if (officeQueryError) { | |
console.error("Error fetching office details:", officeQueryError); | |
return null; | |
} | |
return officeData as Office; | |
} | |
try { | |
// Create a new office for this manager | |
const { data: newOffice, error: officeError } = await supabase | |
.from('offices') | |
.insert({ | |
name: officeName, | |
created_by: userId | |
}) | |
.select('*') | |
.single(); | |
if (officeError) { | |
console.error("Failed to create new office:", officeError); | |
return null; | |
} | |
if (!newOffice) { | |
console.error("No office data returned after creation"); | |
return null; | |
} | |
console.log("Created new office:", newOffice); | |
// Add the user as office manager | |
const { error: membershipError } = await supabase | |
.from('office_members') | |
.insert({ | |
user_id: userId, | |
office_id: newOffice.id, | |
role: 'office_manager', | |
status: 'active', | |
joined_at: new Date().toISOString() | |
}); | |
if (membershipError) { | |
console.error("Failed to add user as office manager:", membershipError); | |
// Continue anyway as the office was created successfully | |
} else { | |
console.log("Added user as office manager"); | |
} | |
return newOffice as Office; | |
} catch (err) { | |
console.error("Unexpected error in office creation process:", err); | |
return null; | |
} | |
} | |
} | |
return null; | |
} | |
// Then get the office details | |
const { data: officeData, error: officeError } = await supabase | |
.from('offices') | |
.select('*') | |
.eq('id', memberData.office_id) | |
.single(); | |
if (officeError) { | |
console.error("Error fetching office details:", officeError); | |
return null; | |
} | |
console.log("AuthProvider: Retrieved office", officeData); | |
// If the user is part of an office, we need to update their profile role if they are an office manager | |
if (memberData.role === 'office_manager') { | |
console.log("AuthProvider: User is an office manager, ensuring their profile role matches"); | |
const { error: updateError } = await supabase | |
.from('profiles') | |
.update({ role: 'office_manager' }) | |
.eq('id', userId); | |
if (updateError) { | |
console.error("Error updating user role to office_manager:", updateError); | |
} else { | |
console.log("Updated user role to office_manager"); | |
} | |
} | |
return officeData as Office; | |
} catch (error) { | |
console.error("Unexpected error fetching office:", error); | |
return null; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContext, useContext, useEffect, useState, ReactNode } from "react"; | |
import { supabase } from "@/integrations/supabase/client"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { AuthContextType, User, Session, UserProfile, Office } from "@/types/auth"; | |
import { convertUser, convertSession, fetchUserProfile, fetchUserOffice } from "@/utils/authUtils"; | |
const AuthContext = createContext<AuthContextType | undefined>(undefined); | |
export function AuthProvider({ children }: { children: ReactNode }) { | |
const [user, setUser] = useState<User | null>(null); | |
const [session, setSession] = useState<Session | null>(null); | |
const [profile, setProfile] = useState<UserProfile | null>(null); | |
const [office, setOffice] = useState<Office | null>(null); | |
const [isLoading, setIsLoading] = useState(true); | |
const { toast } = useToast(); | |
// Load profile and office data when user changes | |
const refreshUserProfile = async () => { | |
if (!user) { | |
setProfile(null); | |
setOffice(null); | |
return; | |
} | |
console.log("AuthProvider: Refreshing user profile and office data"); | |
try { | |
const userProfile = await fetchUserProfile(user.id); | |
setProfile(userProfile); | |
if (userProfile) { | |
console.log("AuthProvider: Profile fetched successfully. Role:", userProfile.role); | |
// If user is an office_manager, ensure they have an office | |
if (userProfile.role === 'office_manager') { | |
console.log("AuthProvider: User is an office_manager, checking office membership"); | |
const userOffice = await fetchUserOffice(user.id); | |
setOffice(userOffice); | |
if (!userOffice) { | |
console.log("Warning: User is an office_manager but has no associated office"); | |
} | |
} else { | |
// For non-managers, still check if they belong to an office | |
const userOffice = await fetchUserOffice(user.id); | |
setOffice(userOffice); | |
} | |
} else { | |
console.log("AuthProvider: Failed to fetch profile, will retry on next auth state change"); | |
setProfile(null); | |
setOffice(null); | |
} | |
} catch (error) { | |
console.error("Error in refreshUserProfile:", error); | |
toast({ | |
title: "Profil betöltési hiba", | |
description: "Nem sikerült betölteni a felhasználói profilját", | |
variant: "destructive", | |
}); | |
} | |
}; | |
// Check for pending invitations and process them using RPC | |
const checkForPendingInvites = async (email: string) => { | |
try { | |
console.log("Checking for pending invites for:", email); | |
// Use RPC function to process invitations | |
const { data, error } = await supabase.rpc( | |
'process_user_invitation', | |
{ user_email: email } | |
); | |
if (error) { | |
console.error("Error processing invitations:", error); | |
return; | |
} | |
// Type checking for the RPC response | |
const typedData = data as any; | |
if (typedData && typedData.invitation_processed) { | |
console.log("Invitation processed successfully:", typedData); | |
toast({ | |
title: "Irodához csatlakozás", | |
description: "Sikeresen csatlakoztál egy irodához meghívás alapján.", | |
}); | |
// Refresh profile to get updated data | |
await refreshUserProfile(); | |
} | |
} catch (error) { | |
console.error("Error processing invitations:", error); | |
} | |
}; | |
useEffect(() => { | |
console.log("AuthProvider: Initializing auth state"); | |
setIsLoading(true); | |
// First set up the auth state change listener | |
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, currentSession) => { | |
console.log("AuthProvider: Auth state changed:", event, "User:", currentSession?.user?.email); | |
if (currentSession) { | |
// Convert from Supabase types to our custom types | |
const customSessionData = convertSession(currentSession); | |
setSession(prevSession => customSessionData); | |
setUser(prevUser => customSessionData.user); | |
// Check for pending invitations on sign in or sign up | |
if (event === 'SIGNED_IN' || event === 'USER_UPDATED' || event === 'TOKEN_REFRESHED') { | |
const userEmail = currentSession.user?.email; | |
if (userEmail) { | |
setTimeout(() => { | |
checkForPendingInvites(userEmail); | |
}, 0); | |
} | |
} | |
} else { | |
setSession(null); | |
setUser(null); | |
} | |
if (currentSession?.user) { | |
// Load user profile and office on auth state change | |
setTimeout(() => { | |
// Use setTimeout to avoid potential Supabase auth deadlocks | |
refreshUserProfile(); | |
}, 0); | |
} else { | |
setProfile(null); | |
setOffice(null); | |
} | |
setIsLoading(false); | |
}); | |
// Then check for existing session | |
supabase.auth.getSession().then(async ({ data: { session: currentSession } }) => { | |
console.log("AuthProvider: Got initial session:", currentSession ? "exists" : "none"); | |
if (currentSession) { | |
// Convert from Supabase types to our custom types | |
const customSessionData = convertSession(currentSession); | |
setSession(prevSession => customSessionData); | |
setUser(prevUser => customSessionData.user); | |
} else { | |
setSession(null); | |
setUser(null); | |
} | |
if (currentSession?.user) { | |
// Load user profile and office on initial load | |
setTimeout(() => { | |
// Use setTimeout to avoid potential Supabase auth deadlocks | |
refreshUserProfile(); | |
}, 0); | |
} | |
setIsLoading(false); | |
}); | |
return () => { | |
console.log("AuthProvider: Unsubscribing from auth state changes"); | |
subscription.unsubscribe(); | |
}; | |
}, []); | |
// Bejelentkezés email/jelszó kombinációval | |
const signIn = async (email: string, password: string) => { | |
try { | |
console.log("AuthProvider: Attempting sign in for:", email); | |
const { data, error } = await supabase.auth.signInWithPassword({ | |
email, | |
password, | |
}); | |
if (error) { | |
console.error("AuthProvider: Sign in error:", error.message); | |
toast({ | |
title: "Bejelentkezési hiba", | |
description: error.message, | |
variant: "destructive", | |
}); | |
return { error, success: false }; | |
} | |
console.log("AuthProvider: Sign in successful"); | |
// Convert to our custom types | |
const customUser = convertUser(data.user); | |
const customSession = convertSession(data.session); | |
setUser(prevUser => customUser); | |
setSession(prevSession => customSession); | |
// Check for pending invitations | |
if (email) { | |
setTimeout(() => { | |
checkForPendingInvites(email); | |
}, 0); | |
} | |
// Load user profile and office after sign in | |
setTimeout(() => { | |
refreshUserProfile(); | |
}, 0); | |
return { error: null, success: true }; | |
} catch (error) { | |
console.error("AuthProvider: Unexpected sign in error:", error); | |
toast({ | |
title: "Bejelentkezési hiba", | |
description: "Váratlan hiba történt a bejelentkezés során", | |
variant: "destructive", | |
}); | |
return { error: error as Error, success: false }; | |
} | |
}; | |
// Regisztráció email/jelszó kombinációval | |
const signUp = async ( | |
email: string, | |
password: string, | |
userData?: { | |
first_name?: string; | |
last_name?: string; | |
role?: 'individual_agent' | 'office_manager' | 'agent'; | |
} | |
) => { | |
try { | |
console.log("AuthProvider: Attempting sign up for:", email); | |
const { data, error } = await supabase.auth.signUp({ | |
email, | |
password, | |
options: { | |
data: { | |
first_name: userData?.first_name || '', | |
last_name: userData?.last_name || '', | |
role: userData?.role || 'individual_agent', | |
} | |
} | |
}); | |
if (error) { | |
console.error("AuthProvider: Sign up error:", error.message); | |
toast({ | |
title: "Regisztrációs hiba", | |
description: error.message, | |
variant: "destructive", | |
}); | |
return { error, success: false }; | |
} | |
if (data.session) { | |
console.log("AuthProvider: Sign up successful with immediate session"); | |
setUser(prevUser => convertUser(data.user)); | |
setSession(prevSession => convertSession(data.session)); | |
// Check for pending invitations | |
if (email) { | |
setTimeout(() => { | |
checkForPendingInvites(email); | |
}, 0); | |
} | |
// Load user profile after sign up if there's a session | |
setTimeout(() => { | |
refreshUserProfile(); | |
}, 0); | |
} else { | |
console.log("AuthProvider: Sign up successful, email confirmation required"); | |
toast({ | |
title: "Sikeres regisztráció", | |
description: "Kérjük, erősítse meg email címét a bejelentkezéshez", | |
}); | |
} | |
return { error: null, success: true }; | |
} catch (error) { | |
console.error("AuthProvider: Unexpected sign up error:", error); | |
toast({ | |
title: "Regisztrációs hiba", | |
description: "Váratlan hiba történt a regisztráció során", | |
variant: "destructive", | |
}); | |
return { error: error as Error, success: false }; | |
} | |
}; | |
// Kijelentkezés | |
const signOut = async () => { | |
console.log("AuthProvider: Signing out"); | |
await supabase.auth.signOut(); | |
setUser(null); | |
setSession(null); | |
setProfile(null); | |
setOffice(null); | |
}; | |
return ( | |
<AuthContext.Provider | |
value={{ | |
user, | |
session, | |
profile, | |
office, | |
isLoading, | |
signIn, | |
signUp, | |
signOut, | |
refreshUserProfile, | |
}} | |
> | |
{children} | |
</AuthContext.Provider> | |
); | |
} | |
// Hook for easy context usage | |
export const useAuth = () => { | |
const context = useContext(AuthContext); | |
if (context === undefined) { | |
throw new Error("useAuth must be used within an AuthProvider"); | |
} | |
return context; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState } from 'react'; | |
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; | |
import { Label } from '@/components/ui/label'; | |
import { Input } from '@/components/ui/input'; | |
import { Button } from '@/components/ui/button'; | |
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; | |
import { useToast } from '@/hooks/use-toast'; | |
import { supabase } from '@/integrations/supabase/client'; | |
import { getUserIdByEmail } from './officeUtils'; | |
interface InviteDialogProps { | |
open: boolean; | |
onOpenChange: (open: boolean) => void; | |
officeId: string; | |
onMemberInvited: () => void; | |
} | |
export function InviteDialog({ | |
open, | |
onOpenChange, | |
officeId, | |
onMemberInvited | |
}: InviteDialogProps) { | |
const [email, setEmail] = useState(''); | |
const [role, setRole] = useState<'agent' | 'office_manager'>('agent'); | |
const [isSubmitting, setIsSubmitting] = useState(false); | |
const { toast } = useToast(); | |
const resetForm = () => { | |
setEmail(''); | |
setRole('agent'); | |
setIsSubmitting(false); | |
}; | |
const handleClose = () => { | |
resetForm(); | |
onOpenChange(false); | |
}; | |
const handleInvite = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (!email || !role) { | |
toast({ | |
title: "Hiányzó adatok", | |
description: "Kérjük adja meg az email címet és válasszon szerepkört.", | |
variant: "destructive", | |
}); | |
return; | |
} | |
setIsSubmitting(true); | |
try { | |
// First check if this user already exists | |
const userId = await getUserIdByEmail(email); | |
if (userId) { | |
// User exists, check if they're already a member of this office | |
const { data: existingMember, error: memberCheckError } = await supabase | |
.from('office_members') | |
.select('id, status') | |
.eq('user_id', userId) | |
.eq('office_id', officeId) | |
.single(); | |
if (memberCheckError && memberCheckError.code !== 'PGRST116') { // PGRST116 is "not found" error | |
throw new Error(memberCheckError.message); | |
} | |
if (existingMember) { | |
if (existingMember.status === 'active') { | |
toast({ | |
title: "Felhasználó már tag", | |
description: "Ez a felhasználó már tagja az irodának.", | |
variant: "destructive", | |
}); | |
setIsSubmitting(false); | |
return; | |
} else { | |
toast({ | |
title: "Meghívás már függőben", | |
description: "Ennek a felhasználónak már van függőben lévő meghívása.", | |
variant: "destructive", | |
}); | |
setIsSubmitting(false); | |
return; | |
} | |
} | |
} | |
// Create invitation using RPC function | |
const { data, error } = await supabase.rpc( | |
'create_office_invite', | |
{ | |
invitation_email: email, | |
invitation_office_id: officeId, | |
invitation_role: role | |
} | |
); | |
if (error) { | |
throw new Error(error.message); | |
} | |
toast({ | |
title: "Meghívó elküldve", | |
description: "A felhasználót sikeresen meghívtad az irodába.", | |
}); | |
onMemberInvited(); | |
handleClose(); | |
} catch (error: any) { | |
console.error("Error inviting member:", error); | |
toast({ | |
title: "Hiba történt", | |
description: error.message || "Nem sikerült elküldeni a meghívót.", | |
variant: "destructive", | |
}); | |
} finally { | |
setIsSubmitting(false); | |
} | |
}; | |
return ( | |
<Dialog open={open} onOpenChange={handleClose}> | |
<DialogContent className="sm:max-w-[425px]"> | |
<DialogHeader> | |
<DialogTitle>Új tag meghívása</DialogTitle> | |
</DialogHeader> | |
<form onSubmit={handleInvite} className="space-y-4"> | |
<div className="space-y-2"> | |
<Label htmlFor="email">Email cím</Label> | |
<Input | |
id="email" | |
type="email" | |
placeholder="pelda@email.com" | |
value={email} | |
onChange={(e) => setEmail(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label>Jogosultság</Label> | |
<RadioGroup value={role} onValueChange={(val) => setRole(val as 'agent' | 'office_manager')}> | |
<div className="flex items-center space-x-2"> | |
<RadioGroupItem value="agent" id="agent" /> | |
<Label htmlFor="agent">Ingatlanközvetítő</Label> | |
</div> | |
<div className="flex items-center space-x-2"> | |
<RadioGroupItem value="office_manager" id="office_manager" /> | |
<Label htmlFor="office_manager">Irodavezető</Label> | |
</div> | |
</RadioGroup> | |
</div> | |
<div className="flex justify-end space-x-2 pt-4"> | |
<Button variant="outline" type="button" onClick={handleClose}> | |
Mégse | |
</Button> | |
<Button type="submit" disabled={isSubmitting}> | |
{isSubmitting ? "Meghívás..." : "Meghívás"} | |
</Button> | |
</div> | |
</form> | |
</DialogContent> | |
</Dialog> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect } from "react"; | |
import { useNavigate, useLocation } from "react-router-dom"; | |
import { CardContent } from "@/components/ui/card"; | |
import { useAuth } from "@/contexts/AuthContext"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { AuthCard } from "@/components/auth/AuthCard"; | |
import { MessageAlert } from "@/components/auth/MessageAlert"; | |
import { LoginForm } from "@/components/auth/LoginForm"; | |
interface LocationState { | |
from?: string; | |
message?: string; | |
error?: string; | |
} | |
export default function LoginPage() { | |
const [email, setEmail] = useState(""); | |
const [password, setPassword] = useState(""); | |
const [isSubmitting, setIsSubmitting] = useState(false); | |
const navigate = useNavigate(); | |
const location = useLocation(); | |
const { user, signIn } = useAuth(); | |
const { toast } = useToast(); | |
// State from location | |
const state = location.state as LocationState; | |
const from = state?.from || "/"; | |
const message = state?.message; | |
const error = state?.error; | |
console.log("LoginPage: Rendered", { from, message, error, isAuthenticated: !!user }); | |
// If already authenticated, redirect | |
useEffect(() => { | |
if (user) { | |
console.log("LoginPage: User already authenticated, redirecting to:", from); | |
navigate(from, { replace: true }); | |
} | |
}, [user, navigate, from]); | |
const handleLogin = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
setIsSubmitting(true); | |
try { | |
console.log("LoginPage: Attempting login"); | |
const { success, error } = await signIn(email, password); | |
if (success) { | |
console.log("LoginPage: Login successful, redirecting to:", from); | |
toast({ | |
title: "Sikeres bejelentkezés", | |
description: "Üdvözöljük a rendszerben!", | |
}); | |
navigate(from, { replace: true }); | |
} else if (error) { | |
console.error("LoginPage: Login error:", error.message); | |
toast({ | |
title: "Bejelentkezési hiba", | |
description: error.message || "Sikertelen bejelentkezés. Kérjük, ellenőrizze az adatokat.", | |
variant: "destructive", | |
}); | |
} | |
} catch (err) { | |
console.error("LoginPage: Unexpected error during login:", err); | |
toast({ | |
title: "Hiba történt", | |
description: "A bejelentkezés során hiba történt. Kérjük, próbálja újra később.", | |
variant: "destructive", | |
}); | |
} finally { | |
setIsSubmitting(false); | |
} | |
}; | |
return ( | |
<AuthCard | |
title="Bejelentkezés" | |
description="Jelentkezz be az ingatlan kezelő rendszerbe" | |
> | |
<MessageAlert message={message} error={error} /> | |
<CardContent> | |
<LoginForm | |
email={email} | |
setEmail={setEmail} | |
password={password} | |
setPassword={setPassword} | |
handleLogin={handleLogin} | |
isSubmitting={isSubmitting} | |
/> | |
</CardContent> | |
</AuthCard> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Button } from '@/components/ui/button'; | |
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; | |
import { UserProfile } from '@/types/auth'; | |
export interface NoOfficeMessageProps { | |
profile: UserProfile | null; | |
onRefreshProfile: () => Promise<void>; | |
} | |
export const NoOfficeMessage = ({ profile, onRefreshProfile }: NoOfficeMessageProps) => { | |
return ( | |
<Alert className="mb-6"> | |
<AlertTitle>Nincs iroda</AlertTitle> | |
<AlertDescription className="space-y-3"> | |
<p> | |
{profile?.role === 'office_manager' | |
? 'Nincs még létrehozva iroda. Mint irodavezető, hozzon létre egy új irodát.' | |
: 'Ön még nem tagja egyetlen irodának sem. Kérje meg az irodavezetőjét, hogy hívja meg a rendszerbe.'} | |
</p> | |
{profile?.role === 'office_manager' && ( | |
<Button | |
size="sm" | |
onClick={() => onRefreshProfile()} | |
> | |
Iroda létrehozása | |
</Button> | |
)} | |
</AlertDescription> | |
</Alert> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect } from "react"; | |
import { Card, CardContent } from "@/components/ui/card"; | |
import { Button } from "@/components/ui/button"; | |
import { Building, MapPin, Phone, Mail, Globe, Upload } from "lucide-react"; | |
import { Input } from "@/components/ui/input"; | |
import { Textarea } from "@/components/ui/textarea"; | |
import { Label } from "@/components/ui/label"; | |
import { supabase } from "@/lib/supabaseClient"; | |
import { useAuth } from "@/contexts/AuthContext"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { AspectRatio } from "@/components/ui/aspect-ratio"; | |
import { Skeleton } from "@/components/ui/skeleton"; | |
export function OfficeInfo() { | |
const [isEditing, setIsEditing] = useState(false); | |
const [isLoading, setIsLoading] = useState(false); | |
const [isSaving, setIsSaving] = useState(false); | |
const { user, office, profile, refreshUserProfile } = useAuth(); | |
const { toast } = useToast(); | |
const [uploadingLogo, setUploadingLogo] = useState(false); | |
const [formData, setFormData] = useState({ | |
name: '', | |
address: '', | |
phone: '', | |
email: '', | |
website: '', | |
logo_url: '', | |
}); | |
useEffect(() => { | |
if (office) { | |
setFormData({ | |
name: office.name || '', | |
address: office.address || '', | |
phone: office.phone || '', | |
email: office.email || '', | |
website: office.website || '', | |
logo_url: office.logo_url || '', | |
}); | |
} | |
}, [office]); | |
// Function to create a new office | |
const handleCreateOffice = async () => { | |
if (!user) return; | |
setIsLoading(true); | |
try { | |
const { data, error } = await supabase | |
.from('offices') | |
.insert({ | |
name: 'Új iroda', | |
created_by: user.id | |
}) | |
.select() | |
.single(); | |
if (error) { | |
console.error("Error creating office:", error); | |
toast({ | |
title: "Hiba", | |
description: "Nem sikerült létrehozni az irodát", | |
variant: "destructive", | |
}); | |
return; | |
} | |
if (!data) { | |
toast({ | |
title: "Hiba", | |
description: "Nem sikerült létrehozni az irodát", | |
variant: "destructive", | |
}); | |
return; | |
} | |
// Add the creator as an office manager | |
const { error: memberError } = await supabase | |
.from('office_members') | |
.insert({ | |
office_id: data.id, | |
user_id: user.id, | |
role: 'office_manager', | |
status: 'active', | |
joined_at: new Date().toISOString() | |
}); | |
if (memberError) { | |
console.error("Error adding user as office manager:", memberError); | |
// We created the office but failed to add the user as a member | |
// In a real app, we might want to handle this better | |
} | |
// Refresh user profile to get updated office information | |
await refreshUserProfile(); | |
toast({ | |
title: "Iroda létrehozva", | |
description: "Az iroda sikeresen létrejött", | |
}); | |
setIsEditing(true); | |
} catch (error) { | |
console.error("Unexpected error creating office:", error); | |
toast({ | |
title: "Hiba", | |
description: "Váratlan hiba történt az iroda létrehozása során", | |
variant: "destructive", | |
}); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
// Function to handle logo upload | |
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { | |
if (!office || !e.target.files || e.target.files.length === 0) return; | |
const file = e.target.files[0]; | |
const fileExt = file.name.split('.').pop(); | |
const filePath = `office-logos/${office.id}-${Math.random().toString(36).substring(2)}.${fileExt}`; | |
setUploadingLogo(true); | |
try { | |
// Upload the file to Supabase Storage | |
const { error: uploadError } = await supabase.storage | |
.from('logos') | |
.upload(filePath, file); | |
if (uploadError) { | |
console.error("Error uploading logo:", uploadError); | |
toast({ | |
title: "Hiba", | |
description: "Nem sikerült feltölteni a logót", | |
variant: "destructive", | |
}); | |
return; | |
} | |
// Get the public URL | |
const { data: publicURL } = supabase.storage | |
.from('logos') | |
.getPublicUrl(filePath); | |
if (publicURL) { | |
// Update form data with the logo URL | |
setFormData({ | |
...formData, | |
logo_url: publicURL.publicUrl | |
}); | |
toast({ | |
title: "Sikeres feltöltés", | |
description: "A logó sikeresen feltöltve", | |
}); | |
} | |
} catch (error) { | |
console.error("Unexpected error uploading logo:", error); | |
toast({ | |
title: "Hiba", | |
description: "Váratlan hiba történt a logó feltöltése során", | |
variant: "destructive", | |
}); | |
} finally { | |
setUploadingLogo(false); | |
} | |
}; | |
// Function to update the office | |
const handleSave = async () => { | |
if (!office) return; | |
setIsSaving(true); | |
try { | |
const { error } = await supabase | |
.from('offices') | |
.update({ | |
name: formData.name, | |
address: formData.address, | |
phone: formData.phone, | |
email: formData.email, | |
website: formData.website, | |
logo_url: formData.logo_url, | |
updated_at: new Date().toISOString() | |
}) | |
.eq('id', office.id); | |
if (error) { | |
console.error("Error updating office:", error); | |
toast({ | |
title: "Hiba", | |
description: "Nem sikerült frissíteni az iroda adatait", | |
variant: "destructive", | |
}); | |
return; | |
} | |
// Refresh user profile to get updated office information | |
await refreshUserProfile(); | |
toast({ | |
title: "Adatok mentve", | |
description: "Az iroda adatai sikeresen frissítve", | |
}); | |
setIsEditing(false); | |
} catch (error) { | |
console.error("Unexpected error updating office:", error); | |
toast({ | |
title: "Hiba", | |
description: "Váratlan hiba történt az iroda adatainak frissítése során", | |
variant: "destructive", | |
}); | |
} finally { | |
setIsSaving(false); | |
} | |
}; | |
// If the user is an individual agent and not part of an office | |
if (!office && profile?.role === 'individual_agent') { | |
return ( | |
<Card> | |
<CardContent className="p-6"> | |
<div className="text-center p-8"> | |
<h3 className="text-lg font-medium mb-2">Nincs irodája</h3> | |
<p className="text-muted-foreground"> | |
Ön egyéni ingatlanközvetítőként regisztrált. Nincs szüksége iroda kezelésére. | |
</p> | |
</div> | |
</CardContent> | |
</Card> | |
); | |
} | |
// If the user is an office manager but doesn't have an office yet | |
if (!office && profile?.role === 'office_manager') { | |
return ( | |
<Card> | |
<CardContent className="p-6"> | |
<div className="text-center p-8"> | |
<h3 className="text-lg font-medium mb-2">Nincs még irodája</h3> | |
<p className="text-muted-foreground mb-4"> | |
Hozzon létre egy új irodát, hogy elkezdhesse a munkát! | |
</p> | |
<Button | |
onClick={handleCreateOffice} | |
disabled={isLoading} | |
> | |
{isLoading ? "Létrehozás..." : "Iroda létrehozása"} | |
</Button> | |
</div> | |
</CardContent> | |
</Card> | |
); | |
} | |
// If the user has an office and is viewing or editing it | |
return ( | |
<Card> | |
<CardContent className="p-6"> | |
<div className="flex justify-between items-center mb-6"> | |
<h2 className="text-xl font-semibold">Iroda adatai</h2> | |
{profile?.role === 'office_manager' && !isEditing && ( | |
<Button onClick={() => setIsEditing(true)}>Szerkesztés</Button> | |
)} | |
{isEditing && ( | |
<div className="flex space-x-2"> | |
<Button variant="outline" onClick={() => setIsEditing(false)}>Mégse</Button> | |
<Button onClick={handleSave} disabled={isSaving}> | |
{isSaving ? "Mentés..." : "Mentés"} | |
</Button> | |
</div> | |
)} | |
</div> | |
{isEditing ? ( | |
<div className="space-y-4"> | |
{/* Logo upload section */} | |
<div className="space-y-2"> | |
<Label>Iroda logó</Label> | |
{formData.logo_url ? ( | |
<div className="mb-4"> | |
<AspectRatio ratio={16 / 9} className="bg-muted"> | |
<img | |
src={formData.logo_url} | |
alt="Iroda logó" | |
className="rounded-md object-contain w-full h-full" | |
/> | |
</AspectRatio> | |
</div> | |
) : null} | |
<div className="border-2 border-dashed rounded-md p-6 flex flex-col items-center justify-center"> | |
<input | |
type="file" | |
id="logo-upload" | |
accept="image/*" | |
className="hidden" | |
onChange={handleLogoUpload} | |
disabled={uploadingLogo} | |
/> | |
<Upload className="h-8 w-8 text-muted-foreground mb-2" /> | |
<p className="text-sm text-muted-foreground mb-1"> | |
{uploadingLogo ? "Feltöltés folyamatban..." : "Húzza ide a képet vagy"} | |
</p> | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={() => document.getElementById('logo-upload')?.click()} | |
disabled={uploadingLogo} | |
> | |
Tallózás | |
</Button> | |
</div> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="office-name">Iroda neve</Label> | |
<Input | |
id="office-name" | |
value={formData.name} | |
onChange={(e) => setFormData({ ...formData, name: e.target.value })} | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="office-address">Cím</Label> | |
<Textarea | |
id="office-address" | |
value={formData.address} | |
onChange={(e) => setFormData({ ...formData, address: e.target.value })} | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="office-phone">Telefonszám</Label> | |
<Input | |
id="office-phone" | |
value={formData.phone} | |
onChange={(e) => setFormData({ ...formData, phone: e.target.value })} | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="office-email">Email cím</Label> | |
<Input | |
id="office-email" | |
type="email" | |
value={formData.email} | |
onChange={(e) => setFormData({ ...formData, email: e.target.value })} | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="office-website">Weboldal</Label> | |
<Input | |
id="office-website" | |
value={formData.website} | |
onChange={(e) => setFormData({ ...formData, website: e.target.value })} | |
/> | |
</div> | |
</div> | |
) : ( | |
<div className="space-y-4"> | |
{office ? ( | |
<> | |
{office.logo_url && ( | |
<div className="mb-4"> | |
<AspectRatio ratio={16 / 9} className="bg-muted"> | |
<img | |
src={office.logo_url} | |
alt="Iroda logó" | |
className="rounded-md object-contain w-full h-full" | |
/> | |
</AspectRatio> | |
</div> | |
)} | |
<div className="flex items-center"> | |
<Building className="h-5 w-5 text-muted-foreground mr-2" /> | |
<p>{office.name}</p> | |
</div> | |
{office.address && ( | |
<div className="flex items-center"> | |
<MapPin className="h-5 w-5 text-muted-foreground mr-2" /> | |
<p>{office.address}</p> | |
</div> | |
)} | |
{office.phone && ( | |
<div className="flex items-center"> | |
<Phone className="h-5 w-5 text-muted-foreground mr-2" /> | |
<p>{office.phone}</p> | |
</div> | |
)} | |
{office.email && ( | |
<div className="flex items-center"> | |
<Mail className="h-5 w-5 text-muted-foreground mr-2" /> | |
<p>{office.email}</p> | |
</div> | |
)} | |
{office.website && ( | |
<div className="flex items-center"> | |
<Globe className="h-5 w-5 text-muted-foreground mr-2" /> | |
<p>{office.website}</p> | |
</div> | |
)} | |
</> | |
) : ( | |
<p className="text-center text-muted-foreground">Nincs elérhető irodai információ</p> | |
)} | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface OfficeInvitation { | |
id: string; | |
email: string; | |
office_id: string; | |
role: 'office_manager' | 'agent' | 'individual_agent'; | |
status: 'pending' | 'accepted' | 'rejected'; | |
created_at: string; | |
updated_at: string; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState } from "react"; | |
import { Card, CardContent } from "@/components/ui/card"; | |
import { Button } from "@/components/ui/button"; | |
import { TeamManagement } from "@/components/office/TeamManagement"; | |
import { OfficeInfo } from "@/components/office/OfficeInfo"; | |
import { PerformanceReport } from "@/components/office/PerformanceReport"; | |
import { Icon } from "@/components/ui/LucideIcon"; | |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
const OfficeSettings = () => { | |
const [activeTab, setActiveTab] = useState("team"); | |
return <div className="container mx-auto px-4 py-8"> | |
<h1 className="text-3xl font-bold mb-6">Iroda & Csapatkezelés</h1> | |
{/* Desktop and Mobile Navigation merged to single tabs interface */} | |
<Tabs defaultValue={activeTab} onValueChange={setActiveTab} className="w-full mb-6"> | |
<TabsList className="flex flex-wrap justify-start mb-6 w-full overflow-x-auto"> | |
<TabsTrigger value="team" className="flex items-center gap-1"> | |
<Icon icon="Users" className="h-4 w-4 hidden sm:inline" /> | |
Csapatkezelés | |
</TabsTrigger> | |
<TabsTrigger value="roles" className="flex items-center gap-1"> | |
<Icon icon="Shield" className="h-4 w-4 hidden sm:inline" /> | |
Jogosultságok | |
</TabsTrigger> | |
<TabsTrigger value="reports" className="flex items-center gap-1"> | |
<Icon icon="BarChart3" className="h-4 w-4 hidden sm:inline" /> | |
Riportok | |
</TabsTrigger> | |
<TabsTrigger value="leads" className="flex items-center gap-1"> | |
<Icon icon="Target" className="h-4 w-4 hidden sm:inline" /> | |
Lead kezelés | |
</TabsTrigger> | |
<TabsTrigger value="documents" className="flex items-center gap-1"> | |
<Icon icon="File" className="h-4 w-4 hidden sm:inline" /> | |
Dokumentumtár | |
</TabsTrigger> | |
<TabsTrigger value="info" className="flex items-center gap-1"> | |
<Icon icon="Building" className="h-4 w-4 hidden sm:inline" /> | |
Iroda adatai | |
</TabsTrigger> | |
<TabsTrigger value="finance" className="flex items-center gap-1"> | |
<Icon icon="DollarSign" className="h-4 w-4 hidden sm:inline" /> | |
Pénzügyek | |
</TabsTrigger> | |
</TabsList> | |
{/* Content area */} | |
<TabsContent value="team"> | |
<TeamManagement /> | |
</TabsContent> | |
<TabsContent value="roles"> | |
<Card> | |
<CardContent className="p-6"> | |
<h2 className="text-xl font-semibold mb-4">Jogosultság kezelés</h2> | |
<p className="text-muted-foreground mb-4">Itt kezelheti a szerepköröket és jogosultságokat (Admin, Ügynök, Irodavezető, stb.)</p> | |
<div className="mt-4"> | |
<Button variant="outline" className="mr-2">Új szerepkör létrehozása</Button> | |
<Button variant="outline">Jogosultság hozzárendelése</Button> | |
</div> | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="reports"> | |
<PerformanceReport /> | |
</TabsContent> | |
<TabsContent value="leads"> | |
<Card> | |
<CardContent className="p-6"> | |
<h2 className="text-xl font-semibold mb-4">Lead szétosztás és körzetkezelés</h2> | |
<p className="text-muted-foreground mb-4">Leadek kiosztása ügynökök között és körzetek kezelése</p> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"> | |
<Card> | |
<CardContent className="p-4"> | |
<h3 className="font-semibold mb-2">Automatikus lead allokáció</h3> | |
<p className="text-sm text-muted-foreground">Beállítások az új leadek automatikus szétosztásához</p> | |
<Button className="mt-4">Szabályok beállítása</Button> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardContent className="p-4"> | |
<h3 className="font-semibold mb-2">Körzetek kezelése</h3> | |
<p className="text-sm text-muted-foreground">Földrajzi körzetek hozzárendelése ügynökökhöz</p> | |
<Button className="mt-4">Körzetek kezelése</Button> | |
</CardContent> | |
</Card> | |
</div> | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="documents"> | |
<Card> | |
<CardContent className="p-6"> | |
<h2 className="text-xl font-semibold mb-4">Dokumentumtár</h2> | |
<p className="text-muted-foreground mb-4">Szerződésminták, oktatóanyagok és egyéb dokumentumok kezelése</p> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"> | |
<Card> | |
<CardContent className="p-4"> | |
<h3 className="font-semibold mb-2">Dokumentumok</h3> | |
<p className="text-sm text-muted-foreground">Szerződésminták, oktatóanyagok és egyéb dokumentumok</p> | |
<Button className="mt-4">Dokumentumtár megnyitása</Button> | |
</CardContent> | |
</Card> | |
</div> | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="info"> | |
<OfficeInfo /> | |
</TabsContent> | |
<TabsContent value="finance"> | |
<Card> | |
<CardContent className="p-6"> | |
<h2 className="text-xl font-semibold mb-4">Jutalék és pénzügyek kezelése</h2> | |
<p className="text-muted-foreground mb-4">Jutalékok, díjak és pénzügyi kimutatások</p> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"> | |
<Card> | |
<CardContent className="p-4"> | |
<h3 className="font-semibold mb-2">Jutalékszámítás</h3> | |
<p className="text-sm text-muted-foreground">Eladási árak és jutalékok számítása ügynökönként</p> | |
<Button className="mt-4" onClick={() => alert('Jutalék beállítás funkció jelenleg fejlesztés alatt!')}>Jutalék beállítások</Button> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardContent className="p-4"> | |
<h3 className="font-semibold mb-2">Pénzügyi áttekintés</h3> | |
<p className="text-sm text-muted-foreground">Az iroda pénzügyi helyzetének áttekintése</p> | |
<Button className="mt-4" onClick={() => alert('Pénzügyi jelentés funkció jelenleg fejlesztés alatt!')}>Pénzügyi jelentés</Button> | |
</CardContent> | |
</Card> | |
</div> | |
</CardContent> | |
</Card> | |
</TabsContent> | |
</Tabs> | |
</div>; | |
}; | |
export default OfficeSettings; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { supabase } from '@/integrations/supabase/client'; | |
import { Office, UserProfile } from '@/types/auth'; | |
import { OfficeInvitation } from '@/types/officeInvitations'; | |
export interface OfficeMember { | |
id: string; | |
user_id: string; | |
office_id: string; | |
role: 'individual_agent' | 'office_manager' | 'agent'; | |
status: string; | |
joined_at: string; | |
invited_at: string; | |
} | |
export interface OfficeMemberWithProfile extends OfficeMember { | |
profile: UserProfile | null; | |
} | |
export interface OfficeWithMembers { | |
office: Office; | |
members: OfficeMemberWithProfile[]; | |
} | |
export const fetchOfficeMembersWithProfiles = async (officeId: string): Promise<OfficeWithMembers | null> => { | |
try { | |
console.log("Fetching office members for office ID:", officeId); | |
// Get the office details | |
const { data: officeData, error: officeError } = await supabase | |
.from('offices') | |
.select('*') | |
.eq('id', officeId) | |
.single(); | |
if (officeError) { | |
console.error("Error fetching office details:", officeError); | |
return null; | |
} | |
if (!officeData) { | |
console.error("No office found with ID:", officeId); | |
return null; | |
} | |
console.log("Office data retrieved:", officeData); | |
// Get office members with simple column names (no table prefix) | |
const { data: membersData, error: membersError } = await supabase | |
.from('office_members') | |
.select('*') | |
.eq('office_id', officeId); | |
if (membersError) { | |
console.error("Error fetching office members:", membersError); | |
return null; | |
} | |
console.log("Office members retrieved:", membersData?.length || 0, "members"); | |
if (!membersData || membersData.length === 0) { | |
// Return office with empty members array if no members found | |
return { | |
office: officeData as Office, | |
members: [] | |
}; | |
} | |
// Extract user IDs from the members to fetch their profiles | |
const userIds = membersData.map(member => member.user_id); | |
// Fetch profiles for all these users | |
const { data: profilesData, error: profilesError } = await supabase | |
.from('profiles') | |
.select('*') | |
.in('id', userIds); | |
if (profilesError) { | |
console.error("Error fetching user profiles:", profilesError); | |
// Continue with null profiles rather than failing completely | |
} | |
console.log("Profiles retrieved:", profilesData?.length || 0, "profiles"); | |
// Create a map of user_id -> profile for easy lookup | |
const profilesMap: Record<string, UserProfile> = {}; | |
if (profilesData) { | |
profilesData.forEach(profile => { | |
profilesMap[profile.id] = profile as UserProfile; | |
}); | |
} | |
// Combine member data with profile data | |
const membersWithProfiles = membersData.map(member => { | |
return { | |
...member, | |
profile: profilesMap[member.user_id] || null | |
} as OfficeMemberWithProfile; | |
}); | |
console.log("Members with profiles combined:", membersWithProfiles.length, "members"); | |
return { | |
office: officeData as Office, | |
members: membersWithProfiles | |
}; | |
} catch (error) { | |
console.error("Unexpected error in fetchOfficeMembersWithProfiles:", error); | |
return null; | |
} | |
}; | |
/** | |
* Fetch a user's ID by their email address | |
*/ | |
export const getUserIdByEmail = async (email: string): Promise<string | null> => { | |
try { | |
const { data, error } = await supabase | |
.rpc('get_user_id_by_email', { email_address: email }); | |
if (error) { | |
console.error("Error fetching user ID by email:", error); | |
return null; | |
} | |
return data as string; | |
} catch (error) { | |
console.error("Unexpected error in getUserIdByEmail:", error); | |
return null; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Navigate, useLocation } from "react-router-dom"; | |
import { useAuth } from "@/contexts/AuthContext"; | |
import { ReactNode, useEffect } from "react"; | |
type ProtectedRouteProps = { | |
children: ReactNode; | |
requiredRoles?: Array<'individual_agent' | 'office_manager' | 'agent'>; | |
}; | |
export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) { | |
const { isLoading, user, profile, refreshUserProfile } = useAuth(); | |
const location = useLocation(); | |
// Get user role from profile | |
const userRole = profile?.role; | |
const authState = { | |
isLoading, | |
isAuthenticated: !!user, | |
userRole, | |
requiredRoles, | |
path: location.pathname | |
}; | |
console.log("ProtectedRoute: Current auth state", authState); | |
// If no profile is loaded yet but we have a user, try refreshing once | |
useEffect(() => { | |
if (user && !profile) { | |
console.log("ProtectedRoute: User is authenticated but no profile loaded, attempting refresh"); | |
refreshUserProfile(); | |
} | |
}, [user, profile, refreshUserProfile]); | |
// If the auth state is still loading, show a loading state | |
if (isLoading) { | |
return <div className="flex h-screen w-full items-center justify-center"> | |
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div> | |
</div>; | |
} | |
// If the user is not authenticated, redirect to login | |
if (!user) { | |
return <Navigate to="/login" state={{ from: location }} replace />; | |
} | |
// Check if specific roles are required for this route | |
if (requiredRoles && requiredRoles.length > 0) { | |
// If the profile is still loading or undefined, wait | |
if (!profile) { | |
console.log("ProtectedRoute: User authenticated but profile not loaded yet, showing loading state"); | |
return <div className="flex h-screen w-full items-center justify-center"> | |
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div> | |
</div>; | |
} | |
// If user doesn't have the required role, redirect to the dashboard | |
if (!userRole || !requiredRoles.includes(userRole)) { | |
console.log(`ProtectedRoute: User lacks required role. Has: ${userRole}, needs one of:`, requiredRoles); | |
return <Navigate to="/" replace />; | |
} | |
} | |
console.log("ProtectedRoute: User authenticated, showing content"); | |
// If the user is authenticated and has the required role (if any), show the content | |
return <>{children}</>; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState } from "react"; | |
import { useNavigate } from "react-router-dom"; | |
import { CardContent } from "@/components/ui/card"; | |
import { useAuth } from "@/contexts/AuthContext"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { AuthCard } from "@/components/auth/AuthCard"; | |
import { SignupForm } from "@/components/auth/SignupForm"; | |
export default function SignupPage() { | |
const [email, setEmail] = useState(""); | |
const [password, setPassword] = useState(""); | |
const [confirmPassword, setConfirmPassword] = useState(""); | |
const [firstName, setFirstName] = useState(""); | |
const [lastName, setLastName] = useState(""); | |
const [role, setRole] = useState<'individual_agent' | 'office_manager'>('individual_agent'); | |
const [isSubmitting, setIsSubmitting] = useState(false); | |
const navigate = useNavigate(); | |
const { signUp } = useAuth(); | |
const { toast } = useToast(); | |
const handleSignup = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (!firstName.trim() || !lastName.trim()) { | |
toast({ | |
title: "Hiba", | |
description: "Kérjük adja meg a teljes nevét", | |
variant: "destructive" | |
}); | |
return; | |
} | |
// Password validation | |
if (password !== confirmPassword) { | |
toast({ | |
title: "Hiba", | |
description: "A megadott jelszavak nem egyeznek", | |
variant: "destructive" | |
}); | |
return; | |
} | |
if (password.length < 8) { | |
toast({ | |
title: "Hiba", | |
description: "A jelszónak legalább 8 karakter hosszúnak kell lennie", | |
variant: "destructive" | |
}); | |
return; | |
} | |
// Check password strength (simplified) | |
const hasUpperCase = /[A-Z]/.test(password); | |
const hasNumber = /[0-9]/.test(password); | |
const hasSpecialChar = /[^A-Za-z0-9]/.test(password); | |
if (!(hasUpperCase || hasNumber || hasSpecialChar)) { | |
toast({ | |
title: "Hiba", | |
description: "Kérjük, használj erősebb jelszót (nagybetű, szám vagy speciális karakter)", | |
variant: "destructive" | |
}); | |
return; | |
} | |
setIsSubmitting(true); | |
try { | |
const { success } = await signUp(email, password, { | |
first_name: firstName, | |
last_name: lastName, | |
role: role | |
}); | |
if (success) { | |
navigate("/login", { | |
replace: true, | |
state: { | |
message: "Sikeres regisztráció! Kérjük, ellenőrizd az email fiókod a megerősítő link miatt." | |
} | |
}); | |
} | |
} finally { | |
setIsSubmitting(false); | |
} | |
}; | |
return ( | |
<AuthCard | |
title="Regisztráció" | |
description="Hozz létre egy fiókot az ingatlan kezelő rendszerben" | |
> | |
<CardContent> | |
<SignupForm | |
email={email} | |
setEmail={setEmail} | |
password={password} | |
setPassword={setPassword} | |
confirmPassword={confirmPassword} | |
setConfirmPassword={setConfirmPassword} | |
firstName={firstName} | |
setFirstName={setFirstName} | |
lastName={lastName} | |
setLastName={setLastName} | |
role={role} | |
setRole={setRole} | |
handleSignup={handleSignup} | |
isSubmitting={isSubmitting} | |
/> | |
</CardContent> | |
</AuthCard> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Link } from "react-router-dom"; | |
import { Button } from "@/components/ui/button"; | |
import { Input } from "@/components/ui/input"; | |
import { Label } from "@/components/ui/label"; | |
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; | |
import { SignupFormProps } from "./types"; | |
import { PasswordStrengthMeter } from "./PasswordStrengthMeter"; | |
export const SignupForm = ({ | |
email, | |
setEmail, | |
password, | |
setPassword, | |
confirmPassword, | |
setConfirmPassword, | |
firstName, | |
setFirstName, | |
lastName, | |
setLastName, | |
role, | |
setRole, | |
handleSignup, | |
isSubmitting | |
}: SignupFormProps) => { | |
return ( | |
<form onSubmit={handleSignup}> | |
<div className="space-y-4"> | |
<div className="grid grid-cols-2 gap-4"> | |
<div className="space-y-2"> | |
<Label htmlFor="firstName">Keresztnév</Label> | |
<Input | |
id="firstName" | |
type="text" | |
value={firstName} | |
onChange={(e) => setFirstName(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="lastName">Vezetéknév</Label> | |
<Input | |
id="lastName" | |
type="text" | |
value={lastName} | |
onChange={(e) => setLastName(e.target.value)} | |
required | |
/> | |
</div> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="email">Email cím</Label> | |
<Input | |
id="email" | |
type="email" | |
placeholder="pelda@email.hu" | |
value={email} | |
onChange={(e) => setEmail(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="space-y-2"> | |
<Label>Felhasználó típusa</Label> | |
<RadioGroup | |
value={role} | |
onValueChange={(value) => setRole(value as 'individual_agent' | 'office_manager')} | |
className="flex flex-col space-y-1" | |
> | |
<div className="flex items-center space-x-2"> | |
<RadioGroupItem value="individual_agent" id="individual" /> | |
<Label htmlFor="individual">Egyéni ingatlanközvetítő</Label> | |
</div> | |
<div className="flex items-center space-x-2"> | |
<RadioGroupItem value="office_manager" id="office" /> | |
<Label htmlFor="office">Iroda vezető</Label> | |
</div> | |
</RadioGroup> | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="password">Jelszó</Label> | |
<Input | |
id="password" | |
type="password" | |
placeholder="••••••••" | |
value={password} | |
onChange={(e) => setPassword(e.target.value)} | |
required | |
/> | |
{password && <PasswordStrengthMeter password={password} />} | |
</div> | |
<div className="space-y-2"> | |
<Label htmlFor="confirmPassword">Jelszó megerősítése</Label> | |
<Input | |
id="confirmPassword" | |
type="password" | |
placeholder="••••••••" | |
value={confirmPassword} | |
onChange={(e) => setConfirmPassword(e.target.value)} | |
required | |
/> | |
{password && confirmPassword && password !== confirmPassword && ( | |
<p className="text-xs text-red-500 mt-1"> | |
A jelszavak nem egyeznek | |
</p> | |
)} | |
</div> | |
</div> | |
<div className="mt-6 flex flex-col space-y-4"> | |
<Button | |
type="submit" | |
className="w-full" | |
disabled={isSubmitting} | |
> | |
{isSubmitting ? "Regisztráció..." : "Regisztráció"} | |
</Button> | |
<div className="text-center text-sm"> | |
Már van fiókod?{" "} | |
<Link | |
to="/login" | |
className="text-blue-600 hover:text-blue-800 font-medium" | |
> | |
Jelentkezz be | |
</Link> | |
</div> | |
</div> | |
</form> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | |
import { Button } from "@/components/ui/button"; | |
import { UserCog, UserX } from "lucide-react"; | |
import { useAuth } from "@/contexts/AuthContext"; | |
import { OfficeMemberWithProfile } from "./officeUtils"; | |
type TeamMemberItemProps = { | |
member: OfficeMemberWithProfile; | |
onRemove: (memberId: string) => void; | |
isOfficeManager: boolean; | |
onMemberChange?: () => void; | |
}; | |
export function TeamMemberItem({ member, onRemove, isOfficeManager }: TeamMemberItemProps) { | |
const { user } = useAuth(); | |
return ( | |
<div | |
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50" | |
> | |
<div className="flex items-center"> | |
<Avatar className="h-10 w-10 mr-3"> | |
<AvatarImage src={member.profile?.avatar_url || undefined} alt="Avatar" /> | |
<AvatarFallback> | |
{member.profile ? | |
`${member.profile.first_name?.[0] || '?'}${member.profile.last_name?.[0] || '?'}` : | |
"??" | |
} | |
</AvatarFallback> | |
</Avatar> | |
<div> | |
<p className="font-medium"> | |
{member.profile ? | |
`${member.profile.first_name || ''} ${member.profile.last_name || ''}` : | |
"Ismeretlen felhasználó" | |
} | |
</p> | |
<p className="text-sm text-muted-foreground"> | |
{member.role === 'office_manager' ? 'Irodavezető' : 'Ingatlanközvetítő'} | |
{member.status === 'pending' && ' (Meghívás folyamatban)'} | |
</p> | |
</div> | |
</div> | |
{isOfficeManager && member.user_id !== user?.id && ( | |
<div className="flex"> | |
<Button variant="ghost" size="icon" className="h-8 w-8"> | |
<UserCog className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant="ghost" | |
size="icon" | |
className="h-8 w-8 text-red-500 hover:text-red-600" | |
onClick={() => onRemove(member.id)} | |
> | |
<UserX className="h-4 w-4" /> | |
</Button> | |
</div> | |
)} | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect } from 'react'; | |
import { Button } from '@/components/ui/button'; | |
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; | |
import { TeamMemberItem } from './TeamMemberItem'; | |
import { InviteDialog } from './InviteDialog'; | |
import { OfficeWithMembers, OfficeMemberWithProfile } from './officeUtils'; | |
import { useToast } from '@/hooks/use-toast'; | |
import { supabase } from '@/integrations/supabase/client'; | |
import { OfficeInvitation } from '@/types/officeInvitations'; | |
export interface TeamMembersListProps { | |
officeWithMembers: OfficeWithMembers; | |
isOfficeManager: boolean; | |
onMemberChange: () => void; | |
} | |
export const TeamMembersList = ({ officeWithMembers, isOfficeManager, onMemberChange }: TeamMembersListProps) => { | |
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false); | |
const [pendingInvites, setPendingInvites] = useState<OfficeInvitation[]>([]); | |
const { office, members } = officeWithMembers; | |
const { toast } = useToast(); | |
const activeMembers = members.filter(member => member.status === 'active'); | |
const pendingMembers = members.filter(member => member.status === 'pending'); | |
// Load pending invitations using RPC function | |
useEffect(() => { | |
const loadPendingInvites = async () => { | |
try { | |
// Use RPC function to get pending invites | |
const { data, error } = await supabase.rpc( | |
'get_office_pending_invites', | |
{ office_id_param: office.id } | |
); | |
if (error) { | |
console.error("Error loading pending invites:", error); | |
return; | |
} | |
// Ensure we have proper typing for the invites | |
if (data && Array.isArray(data)) { | |
setPendingInvites(data as OfficeInvitation[]); | |
} else { | |
setPendingInvites([]); | |
} | |
} catch (error) { | |
console.error("Error in loadPendingInvites:", error); | |
} | |
}; | |
if (office?.id) { | |
loadPendingInvites(); | |
} | |
}, [office?.id]); | |
const handleRemoveMember = async (memberId: string) => { | |
try { | |
const { error } = await supabase | |
.from('office_members') | |
.delete() | |
.eq('id', memberId); | |
if (error) { | |
throw new Error(error.message); | |
} | |
toast({ | |
title: "Sikeres művelet", | |
description: "A tag eltávolítása sikeres volt.", | |
}); | |
// Refresh the member list | |
onMemberChange(); | |
} catch (error) { | |
console.error("Error removing office member:", error); | |
toast({ | |
title: "Hiba történt", | |
description: "Nem sikerült eltávolítani a tagot.", | |
variant: "destructive" | |
}); | |
} | |
}; | |
const handleCancelInvite = async (inviteId: string) => { | |
try { | |
// Use RPC function to delete invite | |
const { error } = await supabase.rpc( | |
'cancel_office_invitation', | |
{ invitation_id: inviteId } | |
); | |
if (error) { | |
throw new Error(error.message); | |
} | |
// Update local state | |
setPendingInvites(current => current.filter(invite => invite.id !== inviteId)); | |
toast({ | |
title: "Sikeres művelet", | |
description: "A meghívó visszavonása sikeres volt.", | |
}); | |
} catch (error) { | |
console.error("Error canceling invitation:", error); | |
toast({ | |
title: "Hiba történt", | |
description: "Nem sikerült visszavonni a meghívót.", | |
variant: "destructive" | |
}); | |
} | |
}; | |
const handleInviteSent = () => { | |
onMemberChange(); | |
// Reload pending invites | |
if (office?.id) { | |
// Use RPC function to get pending invites | |
supabase.rpc( | |
'get_office_pending_invites', | |
{ office_id_param: office.id } | |
).then(({ data }) => { | |
if (data && Array.isArray(data)) { | |
setPendingInvites(data as OfficeInvitation[]); | |
} | |
}); | |
} | |
}; | |
return ( | |
<Card> | |
<CardHeader className="flex flex-row items-center justify-between"> | |
<div> | |
<CardTitle>Csapattagok</CardTitle> | |
<p className="text-sm text-gray-500"> | |
{activeMembers.length} aktív tag{activeMembers.length !== 1 ? 'ok' : ''}{' '} | |
{(pendingMembers.length > 0 || pendingInvites.length > 0) && | |
`és ${pendingMembers.length + pendingInvites.length} függő meghívás`} | |
</p> | |
</div> | |
{isOfficeManager && ( | |
<Button | |
variant="outline" | |
onClick={() => setIsInviteDialogOpen(true)} | |
> | |
Új tag meghívása | |
</Button> | |
)} | |
</CardHeader> | |
<CardContent className="space-y-4"> | |
{members.length === 0 && pendingInvites.length === 0 ? ( | |
<p className="text-center py-6 text-muted-foreground">Nincsenek tagok ebben az irodában</p> | |
) : ( | |
<> | |
{activeMembers.map(member => ( | |
<TeamMemberItem | |
key={member.id} | |
member={member} | |
isOfficeManager={isOfficeManager} | |
onRemove={handleRemoveMember} | |
onMemberChange={onMemberChange} | |
/> | |
))} | |
{(pendingMembers.length > 0 || pendingInvites.length > 0) && ( | |
<div className="mt-6"> | |
<h3 className="text-sm font-medium text-gray-500 mb-3">Függő meghívások</h3> | |
{pendingMembers.map(member => ( | |
<TeamMemberItem | |
key={member.id} | |
member={member} | |
isOfficeManager={isOfficeManager} | |
onRemove={handleRemoveMember} | |
onMemberChange={onMemberChange} | |
/> | |
))} | |
{pendingInvites.map(invite => ( | |
<div key={invite.id} className="flex items-center justify-between py-2 border-b last:border-0"> | |
<div className="flex items-center gap-3"> | |
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500"> | |
{invite.email.charAt(0).toUpperCase()} | |
</div> | |
<div> | |
<p className="font-medium">{invite.email}</p> | |
<p className="text-sm text-gray-500"> | |
{invite.role === 'office_manager' ? 'Irodavezető' : 'Ingatlanközvetítő'} | |
<span className="ml-2 px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded-full text-xs"> | |
Meghívó függőben | |
</span> | |
</p> | |
</div> | |
</div> | |
{isOfficeManager && ( | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={() => handleCancelInvite(invite.id)} | |
> | |
Visszavonás | |
</Button> | |
)} | |
</div> | |
))} | |
</div> | |
)} | |
</> | |
)} | |
</CardContent> | |
<InviteDialog | |
open={isInviteDialogOpen} | |
onOpenChange={setIsInviteDialogOpen} | |
officeId={office.id} | |
onMemberInvited={handleInviteSent} | |
/> | |
</Card> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference types="vite/client" /> | |
// Define the RPC function interface | |
declare namespace Supabase { | |
type RpcFunction = | |
| "get_user_campaign_stats" | |
| "get_user_office_id" | |
| "is_office_manager" | |
| "is_office_member" | |
| "get_user_id_by_email" | |
| "create_office_invite" | |
| "get_office_pending_invites" | |
| "process_user_invitation" | |
| "cancel_office_invitation"; | |
// Define return types for RPC functions | |
interface RpcReturnTypes { | |
// These are the return types for each RPC function | |
"get_user_campaign_stats": { activecount: number; inactivecount: number; platformcount: number }; | |
"get_user_office_id": string; | |
"is_office_manager": boolean; | |
"is_office_member": boolean; | |
"get_user_id_by_email": string; | |
"create_office_invite": string; | |
"get_office_pending_invites": import('./types/officeInvitations').OfficeInvitation[]; | |
"cancel_office_invitation": boolean; | |
"process_user_invitation": { | |
invitation_processed: boolean; | |
office_id?: string; | |
role?: string; | |
message?: string; | |
}; | |
} | |
} | |
// Extend the supabase-js types | |
declare module '@supabase/supabase-js' { | |
interface SupabaseClient<Database = any> { | |
rpc<T extends Supabase.RpcFunction, P extends Record<string, any> = {}>( | |
fn: T, | |
params?: P | |
): import('@supabase/supabase-js').PostgrestSingleResponse<Supabase.RpcReturnTypes[T]>; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment