Skip to content

Instantly share code, notes, and snippets.

@Biggiegoesallthewayup
Created May 15, 2025 11:23
Show Gist options
  • Save Biggiegoesallthewayup/aa1171230463701051e102540fcc5ae0 to your computer and use it in GitHub Desktop.
Save Biggiegoesallthewayup/aa1171230463701051e102540fcc5ae0 to your computer and use it in GitHub Desktop.
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>;
};
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>
);
};
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>
);
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;
}
};
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;
};
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>
);
}
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>
);
}
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>
);
};
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>
);
}
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;
}
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;
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;
}
};
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}</>;
}
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>
);
}
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>
);
};
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>
);
}
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>
);
};
/// <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