diff --git a/app/api/companies/[slug]/members/[userId]/route.ts b/app/api/companies/[slug]/members/[userId]/route.ts index 145b47a1..c1e30786 100644 --- a/app/api/companies/[slug]/members/[userId]/route.ts +++ b/app/api/companies/[slug]/members/[userId]/route.ts @@ -5,6 +5,7 @@ import { companyMemberService } from '@/lib/services/company-member-service' import { CompanyError } from '@/types/company' import { UnifiedCache } from '@/lib/unified-cache-system' import { z } from 'zod' +import { getRoleChangeEmail, sendCompanyEmail } from '@/lib/email/company-emails' // Force Node.js runtime for API routes export const runtime = 'nodejs' @@ -98,12 +99,59 @@ export async function PUT( ) } + // Store old role for email notification + const oldRole = targetMember.role + // Update member role const updatedMember = await companyMemberService.updateMemberRole( targetMember.id, role ) + // Get member's profile information for email + const { data: memberProfile } = await supabase + .from('profiles') + .select('email, first_name, last_name') + .eq('id', userId) + .single() + + // Get requesting user's name for email + const { data: requestingUserProfile } = await supabase + .from('profiles') + .select('first_name, last_name') + .eq('id', user.id) + .single() + + const changedByName = requestingUserProfile?.first_name + ? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim() + : 'a team administrator' + + // Send role change notification email + if (memberProfile?.email && oldRole !== role) { + const memberName = memberProfile.first_name || memberProfile.email.split('@')[0] + const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'https://codeunia.com'}/dashboard/company/${company.slug}` + + const emailContent = getRoleChangeEmail({ + memberName, + companyName: company.name, + oldRole, + newRole: role, + changedBy: changedByName, + dashboardUrl, + }) + + // Send email asynchronously (don't wait for it) + console.log(`📧 Sending role change email to ${memberProfile.email}: ${oldRole} → ${role}`) + sendCompanyEmail({ + to: memberProfile.email, + subject: emailContent.subject, + html: emailContent.html, + }).catch(error => { + console.error('❌ Failed to send role change email:', error) + // Don't fail the request if email fails + }) + } + // Invalidate cache await UnifiedCache.purgeByTags(['content', 'api']) diff --git a/app/dashboard/company/layout.tsx b/app/dashboard/company/layout.tsx index 92ab7759..9d56c39f 100644 --- a/app/dashboard/company/layout.tsx +++ b/app/dashboard/company/layout.tsx @@ -18,6 +18,7 @@ import { } from 'lucide-react' import { useAuth } from '@/lib/hooks/useAuth' import { useRoleProtection } from '@/lib/hooks/useRoleProtection' +import { createClient } from '@/lib/supabase/client' export type SidebarGroupType = { title: string @@ -95,37 +96,68 @@ export default function CompanyDashboardLayout({ ) } - const avatar = - user?.user_metadata?.first_name?.[0]?.toUpperCase() || - user?.email?.[0]?.toUpperCase() || - 'C' - const name = user?.user_metadata?.first_name || user?.email || 'User' - const email = user?.email || 'user@codeunia.com' - return ( - + {children} ) } +interface UserProfile { + id: string + email: string + first_name?: string + last_name?: string + avatar_url?: string +} + // Component that uses CompanyContext to conditionally render sidebar function CompanyDashboardContent({ - avatar, - name, - email, + user, children, }: { - avatar: string - name: string - email: string + user: { id: string; email?: string; user_metadata?: { first_name?: string } } | null children: React.ReactNode }) { const params = useParams() const companySlug = params?.slug as string const { currentCompany, userCompanies, loading, error } = useCompanyContext() + const [userProfile, setUserProfile] = useState(null) + + // Fetch user profile data + useEffect(() => { + async function fetchProfile() { + if (!user?.id) return + + try { + const supabase = createClient() + const { data, error } = await supabase + .from('profiles') + .select('id, email, first_name, last_name, avatar_url') + .eq('id', user.id) + .single() + + if (!error && data) { + setUserProfile(data) + } + } catch (error) { + console.error('Error fetching user profile:', error) + } + } + + fetchProfile() + }, [user?.id]) + + const avatarUrl = userProfile?.avatar_url + const avatarInitial = + userProfile?.first_name?.[0]?.toUpperCase() || + user?.user_metadata?.first_name?.[0]?.toUpperCase() || + user?.email?.[0]?.toUpperCase() || + 'C' + const name = userProfile?.first_name || user?.user_metadata?.first_name || user?.email || 'User' + const email = user?.email || 'user@codeunia.com' // Show loading while company context is loading if (loading) { @@ -213,7 +245,8 @@ function CompanyDashboardContent({ return ( ( + + + + {avatarInitial} + + + ) const [mobileOpen, setMobileOpen] = useState(false) const [collapsed, setCollapsed] = useState(false) const closeSidebar = () => setMobileOpen(false) @@ -155,9 +167,7 @@ export function CompanySidebar({ variant="ghost" className="w-full flex items-center gap-3 rounded-xl p-2 hover:bg-purple-700/20 transition-colors" > -
- {avatar} -
+
{name} @@ -177,9 +187,7 @@ export function CompanySidebar({ >
-
- {avatar} -
+
{name} @@ -397,9 +405,7 @@ export function CompanySidebar({ className="w-full flex items-center gap-3 rounded-xl p-2 hover:bg-purple-700/20 transition-colors data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" title={collapsed ? name : undefined} > -
- {avatar} -
+ {!collapsed && ( <>
@@ -423,9 +429,7 @@ export function CompanySidebar({ >
-
- {avatar} -
+
{name} diff --git a/components/dashboard/TeamManagement.tsx b/components/dashboard/TeamManagement.tsx index 096fb8d2..9c19f493 100644 --- a/components/dashboard/TeamManagement.tsx +++ b/components/dashboard/TeamManagement.tsx @@ -41,7 +41,7 @@ import { } from '@/components/ui/select' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useToast } from '@/components/ui/use-toast' +import { toast } from 'sonner' import { Skeleton } from '@/components/ui/skeleton' import { UserPlus, @@ -86,7 +86,6 @@ export function TeamManagement({ const [inviteRole, setInviteRole] = useState<'admin' | 'editor' | 'viewer'>('viewer') const [newRole, setNewRole] = useState<'owner' | 'admin' | 'editor' | 'viewer'>('viewer') const [submitting, setSubmitting] = useState(false) - const { toast } = useToast() // Check if current user can manage team const canManageTeam = ['owner', 'admin'].includes(currentUserRole) @@ -110,15 +109,11 @@ export function TeamManagement({ setMembers(data.members || []) } catch (error) { console.error('Error fetching members:', error) - toast({ - title: 'Error', - description: 'Failed to load team members', - variant: 'destructive', - }) + toast.error('Failed to load team members') } finally { setLoading(false) } - }, [companySlug, toast]) + }, [companySlug]) useEffect(() => { fetchMembers() @@ -127,11 +122,7 @@ export function TeamManagement({ // Handle invite member const handleInvite = async () => { if (!inviteEmail) { - toast({ - title: 'Error', - description: 'Please enter an email address', - variant: 'destructive', - }) + toast.error('Please enter an email address') return } @@ -150,13 +141,20 @@ export function TeamManagement({ if (!response.ok) { console.error('Invite error response:', data) - throw new Error(data.error || 'Failed to invite member') + + // Check if it's a team limit error + if (data.upgrade_required) { + toast.error('Team Member Limit Reached', { + description: `${data.error} (${data.current_usage}/${data.limit}). Please upgrade your plan to add more members.`, + duration: 6000, + }) + } else { + toast.error(data.error || 'Failed to invite member') + } + return } - toast({ - title: 'Success', - description: 'Team member invited successfully', - }) + toast.success('Team member invited successfully') setInviteDialogOpen(false) setInviteEmail('') @@ -164,11 +162,7 @@ export function TeamManagement({ fetchMembers() } catch (error) { console.error('Error inviting member:', error) - toast({ - title: 'Error', - description: error instanceof Error ? error.message : 'Failed to invite member', - variant: 'destructive', - }) + toast.error(error instanceof Error ? error.message : 'Failed to invite member') } finally { setSubmitting(false) } @@ -195,21 +189,14 @@ export function TeamManagement({ throw new Error(data.error || 'Failed to update role') } - toast({ - title: 'Success', - description: 'Member role updated successfully', - }) + toast.success('Member role updated successfully') setRoleDialogOpen(false) setSelectedMember(null) fetchMembers() } catch (error) { console.error('Error updating role:', error) - toast({ - title: 'Error', - description: error instanceof Error ? error.message : 'Failed to update role', - variant: 'destructive', - }) + toast.error(error instanceof Error ? error.message : 'Failed to update role') } finally { setSubmitting(false) } @@ -234,21 +221,14 @@ export function TeamManagement({ throw new Error(data.error || 'Failed to remove member') } - toast({ - title: 'Success', - description: 'Member removed successfully', - }) + toast.success('Member removed successfully') setRemoveDialogOpen(false) setSelectedMember(null) fetchMembers() } catch (error) { console.error('Error removing member:', error) - toast({ - title: 'Error', - description: error instanceof Error ? error.message : 'Failed to remove member', - variant: 'destructive', - }) + toast.error(error instanceof Error ? error.message : 'Failed to remove member') } finally { setSubmitting(false) } @@ -473,7 +453,7 @@ export function TeamManagement({ Invite Team Member - Send an invitation to join your team. They must have a CodeUnia account. + Send an invitation to join your team. They must have a Codeunia account.
diff --git a/lib/email/company-emails.ts b/lib/email/company-emails.ts index 4c2affab..0f13d52e 100644 --- a/lib/email/company-emails.ts +++ b/lib/email/company-emails.ts @@ -13,7 +13,7 @@ const getEmailTemplate = (content: string) => ` - CodeUnia + Codeunia @@ -23,7 +23,7 @@ const getEmailTemplate = (content: string) => ` @@ -228,6 +228,102 @@ export const getNewCompanyRegistrationNotification = (params: { } } +// Role change notification email +export const getRoleChangeEmail = (params: { + memberName: string + companyName: string + oldRole: string + newRole: string + changedBy: string + dashboardUrl: string +}) => { + const rolePermissions: Record = { + owner: [ + 'Full control over company settings', + 'Manage all team members and roles', + 'Create, edit, and delete all events', + 'Access billing and subscription', + 'View all analytics and reports' + ], + admin: [ + 'Create, edit, and publish events', + 'Manage team members (except owners)', + 'View analytics and reports', + 'Manage company profile' + ], + editor: [ + 'Create and edit draft events', + 'View published events', + 'View basic analytics' + ], + viewer: [ + 'View company events', + 'View basic analytics', + 'Read-only access' + ] + } + + const permissions = rolePermissions[params.newRole.toLowerCase()] || [] + + const content = ` +

+ Your Role Has Been Updated +

+ +

+ Hi ${params.memberName}, +

+ +

+ Your role at ${params.companyName} has been updated by ${params.changedBy}. +

+ +
+
-

CodeUnia

+

Codeunia

+ + + + + + + + +
+ Previous Role: + + ${params.oldRole.charAt(0).toUpperCase() + params.oldRole.slice(1)} +
+ New Role: + + + ${params.newRole.charAt(0).toUpperCase() + params.newRole.slice(1)} + +
+
+ +

+ Your new permissions include: +

+ +
    + ${permissions.map(perm => `
  • ${perm}
  • `).join('')} +
+ + + Go to Dashboard + + +

+ If you have questions about your new role or permissions, please contact your team administrator. +

+ ` + + return { + subject: `Your role at ${params.companyName} has been updated`, + html: getEmailTemplate(content) + } +} + // Send email function using Resend export async function sendCompanyEmail(params: EmailParams) { try {